Completed
Pull Request — master (#150)
by
unknown
06:02
created

IpList::makeArrayOfIps()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 12
ccs 6
cts 6
cp 1
rs 9.8666
c 0
b 0
f 0
cc 3
nc 4
nop 1
crap 3
1
<?php
2
3
namespace PragmaRX\Firewall\Repositories;
4
5
use PragmaRX\Firewall\Support\ServiceInstances;
6
use PragmaRX\Firewall\Vendor\Laravel\Models\Firewall as FirewallModel;
7
8
class IpList
9
{
10
    use ServiceInstances;
11
12
    const IP_ADDRESS_LIST_CACHE_NAME = 'firewall.ip_address_list';
13
14
    /**
15
     * @var \PragmaRX\Firewall\Vendor\Laravel\Models\Firewall
16
     */
17
    private $model;
18
19
    /**
20
     * Create instance of DataRepository.
21
     *
22
     * @param \PragmaRX\Firewall\Vendor\Laravel\Models\Firewall $model
23
     */
24 60
    public function __construct(FirewallModel $model)
25
    {
26 60
        $this->model = $model;
27 60
    }
28
29
    /**
30
     * Get a list of non database ip addresses.
31
     *
32
     * @return array
33
     */
34 56
    private function getNonDatabaseIps()
35
    {
36 56
        return array_merge_recursive(
37
            array_map(function ($ip) {
38 9
                $ip['whitelisted'] = true;
39
40 9
                return $ip;
41 56
            }, $this->formatIpArray($this->config()->get('whitelist'))),
42
43
            array_map(function ($ip) {
44 17
                $ip['whitelisted'] = false;
45
46 17
                return $ip;
47 56
            }, $this->formatIpArray($this->config()->get('blacklist')))
48
        );
49
    }
50
51
    /**
52
     * Remove ip from all array lists.
53
     *
54
     * @param $ip
55
     *
56
     * @return bool
57
     */
58 4
    private function removeFromArrayList($ip)
59
    {
60 4
        return $this->removeFromArrayListType('whitelist', $ip) ||
61 4
            $this->removeFromArrayListType('blacklist', $ip);
62
    }
63
64
    /**
65
     * Remove the ip address from an array list.
66
     *
67
     * @param $type
68
     * @param $ip
69
     *
70
     * @return bool
71
     */
72 4
    private function removeFromArrayListType($type, $ip)
73
    {
74 4
        if (($key = array_search($ip, $data = $this->config()->get($type))) !== false) {
75 4
            unset($data[$key]);
76
77 4
            $this->cache()->forget($ip);
78
79 4
            $this->config()->set($type, $data);
80
81 4
            $this->messages()->addMessage(sprintf('%s removed from %s', $ip, $type));
82
83 4
            return true;
84
        }
85
86 2
        return false;
87
    }
88
89
    /**
90
     * Remove ip from database.
91
     *
92
     * @param \Illuminate\Database\Eloquent\Model $ip
93
     *
94
     * @return bool
95
     */
96 4
    private function removeFromDatabaseList($ip)
97
    {
98 4
        $ip = $this->find($ip);
99
100 4
        $ip->delete();
0 ignored issues
show
Bug introduced by
The call to delete() misses a required argument $ip.

This check looks for function calls that miss required arguments.

Loading history...
Bug introduced by
The call to delete() misses a required argument $key.

This check looks for function calls that miss required arguments.

Loading history...
101
102 4
        $this->cache()->forget($ip->ip_address);
103
104 4
        $this->messages()->addMessage(sprintf('%s removed from %s', $ip, $ip->whitelisted ? 'whitelist' : 'blacklist'));
105 4
    }
106
107
    /**
108
     * Transform a list of ips to a list of models.
109
     *
110
     * @param $ipList
111
     *
112
     * @return \Illuminate\Support\Collection
113
     */
114 56
    private function toModels($ipList)
115
    {
116 56
        $ips = [];
117
118 56
        foreach ($ipList as $ip) {
119 10
            $ips[] = $this->makeModel($ip);
120
        }
121
122 56
        return collect($ips);
123
    }
124
125
    /**
126
     * Make a model instance.
127
     *
128
     * @param $ip
129
     *
130
     * @return \Illuminate\Database\Eloquent\Model
131
     */
132 24
    private function makeModel($ip)
133
    {
134 24
        return $this->model->newInstance($ip);
135
    }
136
137
    /**
138
     * Read a file contents.
139
     *
140
     * @param $file
141
     *
142
     * @return array
143
     */
144
    private function readFile($file)
145
    {
146
        if ($this->fileSystem()->exists($file)) {
147
            $lines = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
148
149
            return $this->makeArrayOfIps($lines);
150
        }
151
152
        return [];
153
    }
154
155
    /**
156
     * Format all ips in an array.
157
     *
158
     * @param $list
159
     *
160
     * @return array
161
     */
162 56
    private function formatIpArray($list)
163
    {
164
        return array_map(function ($ip) {
165 24
            return ['ip_address' => $ip];
166 56
        }, $this->makeArrayOfIps($list));
167
    }
168
169
    /**
170
     * Make a list of arrays from all sort of things.
171
     *
172
     * @param $list
173
     *
174
     * @return array
175
     */
176 56
    private function makeArrayOfIps($list)
177
    {
178 56
        $list = (array) $list ?: [];
179
180 56
        $ips = [];
181
182 56
        foreach ($list as $item) {
183 24
            $ips = array_merge($ips, $this->getIpsFromAnything($item));
184
        }
185
186 56
        return $ips;
187
    }
188
189
    /**
190
     * Get a list of ips from anything.
191
     *
192
     * @param $item
193
     *
194
     * @return array
195
     */
196 24
    private function getIpsFromAnything($item)
197
    {
198 24
        if (starts_with($item, 'country:')) {
0 ignored issues
show
Deprecated Code introduced by
The function starts_with() has been deprecated with message: Str::startsWith() should be used directly instead. Will be removed in Laravel 5.9.

This function has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed from the class and what other function to use instead.

Loading history...
199 2
            return [$item];
200
        }
201
202 22
        $item = $this->ipAddress()->hostToIp($item);
203
204 22
        if ($this->ipAddress()->ipV4Valid($item)) {
205 22
            return [$item];
206
        }
207
208
        return $this->readFile($item);
209
    }
210
211
    /**
212
     * Search for an ip in alist of ips.
213
     *
214
     * @param $ip
215
     * @param $ips
216
     *
217
     * @return null|\Illuminate\Database\Eloquent\Model
218
     */
219 52
    private function ipArraySearch($ip, $ips)
220
    {
221 52
        foreach ($ips as $key => $value) {
222
            if (
223 24
                (isset($value['ip_address']) && $value['ip_address'] == $ip) ||
224 12
                (strval($key) == $ip) ||
225 24
                ($value == $ip)
226
            ) {
227 24
                return $value;
228
            }
229
        }
230 52
    }
231
232
    /**
233
     * Get all IPs from database.
234
     *
235
     * @return \Illuminate\Support\Collection
236
     */
237 56
    private function getAllFromDatabase()
238
    {
239 56
        if ($this->config()->get('use_database')) {
240 17
            return $this->model->all();
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->model->all(); of type Illuminate\Database\Eloq...base\Eloquent\Builder[] adds the type Illuminate\Database\Eloquent\Builder[] to the return on line 240 which is incompatible with the return type documented by PragmaRX\Firewall\Reposi...ist::getAllFromDatabase of type Illuminate\Support\Collection.
Loading history...
241
        } else {
242 39
            return collect([]);
243
        }
244
    }
245
246
    /**
247
     * Merge IP lists.
248
     *
249
     * @param $database_ips
250
     * @param $config_ips
251
     *
252
     * @return \Illuminate\Support\Collection
253
     */
254 56
    private function mergeLists($database_ips, $config_ips)
255
    {
256 56
        return collect($database_ips)
257 56
            ->merge(collect($config_ips));
258
    }
259
260
    /**
261
     * Check if an IP address is in a secondary (black/white) list.
262
     *
263
     * @param $ip_address
264
     *
265
     * @return bool|array
266
     */
267 52
    public function checkSecondaryLists($ip_address)
268
    {
269 52
        foreach ($this->all() as $range) {
270 17
            if ($this->ipAddress()->hostToIp($range->ip_address) == $ip_address || $this->ipAddress()->validRange($ip_address, $range)) {
271 17
                return $range;
272
            }
273
        }
274
275 52
        return false;
276
    }
277
278
    /**
279
     * Add an IP to black or whitelist.
280
     *
281
     * @param $whitelist
282
     * @param $ip
283
     * @param bool $force
284
     *
285
     * @return bool
286
     */
287 43
    public function addToList($whitelist, $ip, $force = false)
288
    {
289 43
        $list = $whitelist
290 20
            ? 'whitelist'
291 43
            : 'blacklist';
292
293 43
        if (!$this->ipAddress()->isValid($ip)) {
294 4
            return false;
295
        }
296
297 39
        $listed = $this->whichList($ip);
298
299 39
        if ($listed == $list) {
300 2
            $this->messages()->addMessage(sprintf('%s is already %s', $ip, $list.'ed'));
301
302 2
            return false;
303
        } else {
304 39
            if (empty($listed) || $force) {
305 39
                if (!empty($listed)) {
306 2
                    $this->remove($ip);
307
                }
308
309 39
                $this->addToProperList($whitelist, $ip);
310
311 39
                $this->messages()->addMessage(sprintf('%s is now %s', $ip, $list.'ed'));
312
313 39
                return true;
314
            }
315
        }
316
317 2
        $this->messages()->addMessage(sprintf('%s is currently %sed', $ip, $listed));
318
319 2
        return false;
320
    }
321
322
    /**
323
     * Tell in which list (black/white) an IP address is.
324
     *
325
     * @param $ip_address
326
     *
327
     * @return null|string
328
     */
329 52
    public function whichList($ip_address)
330
    {
331 52
        if (!$ip_found = $this->find($ip_address)) {
332 52
            if (!$ip_found = $this->findByCountry($ip_address)) {
333 52
                if (!$ip_found = $this->checkSecondaryLists($ip_address)) {
334 52
                    return;
335
                }
336
            }
337
        }
338
339 31
        return !is_null($ip_found)
340 31
            ? ($ip_found['whitelisted'] ? 'whitelist' : 'blacklist')
341 31
            : null;
342
    }
343
344
    /**
345
     * Remove IP from all lists.
346
     *
347
     * @param $ip
348
     *
349
     * @return bool
350
     */
351 8
    public function remove($ip)
352
    {
353 8
        $listed = $this->whichList($ip);
354
355 8
        if (!empty($listed)) {
356 8
            $this->delete($ip);
357
358 8
            return true;
359
        }
360
361 2
        $this->messages()->addMessage(sprintf('%s is not listed', $ip));
362
363 2
        return false;
364
    }
365
366
    /**
367
     * Add ip or range to array list.
368
     *
369
     * @param $whitelist
370
     * @param $ip
371
     *
372
     * @return array|mixed
373
     */
374 24
    private function addToArrayList($whitelist, $ip)
375
    {
376 24
        $data = $this->config()->get($list = $whitelist ? 'whitelist' : 'blacklist');
377
378 24
        $data[] = $ip;
379
380 24
        $this->config()->set($list, $data);
381
382 24
        return $data;
383
    }
384
385
    /**
386
     * Find an IP address in the data source.
387
     *
388
     * @param string $ip
389
     *
390
     * @return mixed
391
     */
392 52
    public function find($ip)
393
    {
394 52
        if ($this->cache()->has($ip)) {
395 17
            return $this->cache()->get($ip);
396
        }
397
398 52
        if ($model = $this->findIp($ip)) {
399 18
            $this->cache()->remember($model);
400
        }
401
402 52
        return $model;
403
    }
404
405
    /**
406
     * Find an IP address by country.
407
     *
408
     * @param $country
409
     *
410
     * @return mixed
411
     */
412 52
    public function findByCountry($country)
413
    {
414 52
        if ($this->config()->get('enable_country_search') && !is_null($country = $this->countries()->makeCountryFromString($country))) {
415 26
            return $this->find($country);
416
        }
417 26
    }
418
419
    /**
420
     * Find a Ip in the data source.
421
     *
422
     * @param string $ip
423
     *
424
     * @return void
425
     */
426 39
    public function addToProperList($whitelist, $ip)
427
    {
428 39
        $this->config()->get('use_database') ?
429 15
            $this->addToDatabaseList($whitelist, $ip) :
430 24
            $this->addToArrayList($whitelist, $ip);
431 39
    }
432
433
    /**
434
     * Delete ip address.
435
     *
436
     * @param $ip
437
     *
438
     * @return bool|void
439
     */
440 8
    public function delete($ip)
441
    {
442 8
        $this->config()->get('use_database') ?
443 4
            $this->removeFromDatabaseList($ip) :
444 4
            $this->removeFromArrayList($ip);
445 8
    }
446
447
    /**
448
     * Get all IP addresses.
449
     *
450
     * @return \Illuminate\Support\Collection
451
     */
452 56
    public function all()
453
    {
454 56
        $cacheTime = $this->config()->get('ip_list_cache_expire_time');
455
456 56
        if ($cacheTime > 0 && $list = $this->cache()->get(static::IP_ADDRESS_LIST_CACHE_NAME)) {
457 1
            return $list;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $list; (Illuminate\Contracts\Cache\Repository) is incompatible with the return type documented by PragmaRX\Firewall\Repositories\IpList::all of type Illuminate\Support\Collection.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
458
        }
459
460 56
        $list = $this->mergeLists(
461 56
            $this->getAllFromDatabase(),
462 56
            $this->toModels($this->getNonDatabaseIps())
463
        );
464
465 56
        if ($cacheTime > 0) {
466 1
            $this->cache()->put(static::IP_ADDRESS_LIST_CACHE_NAME, $list, $cacheTime);
467
        }
468
469 56
        return $list;
470
    }
471
472
    /**
473
     * Find ip address in all lists.
474
     *
475
     * @param $ip
476
     *
477
     * @return \Illuminate\Database\Eloquent\Model|null|static
478
     */
479 52
    private function findIp($ip)
480
    {
481 52
        if ($model = $this->nonDatabaseFind($ip)) {
482 18
            return $model;
483
        }
484
485 52
        if ($this->config()->get('use_database')) {
486 16
            return $this->model->where('ip_address', $ip)->first();
0 ignored issues
show
Documentation Bug introduced by
The method where does not exist on object<PragmaRX\Firewall...aravel\Models\Firewall>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
487
        }
488 36
    }
489
490
    /**
491
     * Find ip in non database lists.
492
     *
493
     * @param $ip
494
     *
495
     * @return \Illuminate\Database\Eloquent\Model
496
     */
497 52
    private function nonDatabaseFind($ip)
498
    {
499 52
        $ips = $this->getNonDatabaseIps();
500
501 52
        if ($ip = $this->ipArraySearch($ip, $ips)) {
502 18
            return $this->makeModel($ip);
503
        }
504 52
    }
505
506
    /**
507
     * Add ip or range to database.
508
     *
509
     * @param $whitelist
510
     * @param $ip
511
     *
512
     * @return \Illuminate\Database\Eloquent\Model
513
     */
514 15
    private function addToDatabaseList($whitelist, $ip)
515
    {
516 15
        $this->model->unguard();
517
518 15
        $model = $this->model->create([
0 ignored issues
show
Bug introduced by
The method create() does not exist on PragmaRX\Firewall\Vendor\Laravel\Models\Firewall. Did you maybe mean created()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
519 15
            'ip_address'  => $ip,
520 15
            'whitelisted' => $whitelist,
521
        ]);
522
523 15
        $this->cache()->remember($model);
524
525 15
        return $model;
526
    }
527
}
528