Passed
Branch master (ecb760)
by Antonio Carlos
05:12
created

IpList::nonDatabaseFind()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 8
ccs 5
cts 5
cp 1
rs 9.4285
c 1
b 0
f 0
cc 2
eloc 4
nc 2
nop 1
crap 2
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 25
    public function __construct(FirewallModel $model)
25
    {
26 25
        $this->model = $model;
27 25
    }
28
29
    /**
30
     * Get a list of non database ip addresses.
31
     *
32
     * @return array
33
     */
34 23
    private function getNonDatabaseIps()
35
    {
36 23
        return array_merge_recursive(
37 23
            array_map(function ($ip) {
38 6
                $ip['whitelisted'] = true;
39
40 6
                return $ip;
41 23
            }, $this->formatIpArray($this->config()->get('whitelist'))),
42
43 23
            array_map(function ($ip) {
44 7
                $ip['whitelisted'] = false;
45
46 7
                return $ip;
47 23
            }, $this->formatIpArray($this->config()->get('blacklist')))
48
        );
49
    }
50
51
    /**
52
     * Remove ip from all array lists.
53
     *
54
     * @param $ipAddress
55
     *
56
     * @return bool
57
     */
58 2
    private function removeFromArrayList($ipAddress)
59
    {
60 2
        return $this->removeFromArrayListType('whitelist', $ipAddress) ||
61 2
            $this->removeFromArrayListType('blacklist', $ipAddress);
62
    }
63
64
    /**
65
     * Remove the ip address from an array list.
66
     *
67
     * @param $type
68
     * @param $ipAddress
69
     *
70
     * @return bool
71
     */
72 2
    private function removeFromArrayListType($type, $ipAddress)
73
    {
74 2
        if (($key = array_search($ipAddress, $data = $this->config()->get($type))) !== false) {
75 2
            unset($data[$key]);
76
77 2
            $this->config()->set($type, $data);
78
79 2
            return true;
80
        }
81
82
        return false;
83
    }
84
85
    /**
86
     * Remove ip from database.
87
     *
88
     * @param \Illuminate\Database\Eloquent\Model $ipAddress
89
     *
90
     * @return bool
91
     */
92 2
    private function removeFromDatabaseList($ipAddress)
93
    {
94 2
        if ($ip = $this->find($ipAddress)) {
95 2
            $ip->delete();
0 ignored issues
show
Bug introduced by
The call to delete() misses a required argument $ipAddress.

This check looks for function calls that miss required arguments.

Loading history...
Bug introduced by
The method delete does only exist in Illuminate\Database\Eloq...all\Repositories\IpList, but not in Illuminate\Contracts\Cache\Repository.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
96
97 2
            $this->cache()->forget($ipAddress);
98
99 2
            return true;
100
        }
101
102
        return false;
103
    }
104
105
    /**
106
     * Transform a list of ips to a list of models.
107
     *
108
     * @param $ipList
109
     *
110
     * @return \Illuminate\Support\Collection
111
     */
112 23
    private function toModels($ipList)
113
    {
114 23
        $ips = [];
115
116 23
        foreach ($ipList as $ip) {
117 6
            $ips[] = $this->makeModel($ip);
118
        }
119
120 23
        return collect($ips);
121
    }
122
123
    /**
124
     * Make a model instance.
125
     *
126
     * @param $ip
127
     *
128
     * @return \Illuminate\Database\Eloquent\Model
129
     */
130 11
    private function makeModel($ip)
131
    {
132 11
        return $this->model->newInstance($ip);
133
    }
134
135
    /**
136
     * Read a file contents.
137
     *
138
     * @param $file
139
     *
140
     * @return array
141
     */
142
    private function readFile($file)
143
    {
144
        if ($this->fileSystem()->exists($file)) {
145
            $lines = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
146
147
            return $this->makeArrayOfIps($lines);
148
        }
149
150
        return [];
151
    }
152
153
    /**
154
     * Format all ips in an array.
155
     *
156
     * @param $list
157
     *
158
     * @return array
159
     */
160
    private function formatIpArray($list)
161
    {
162 23
        return array_map(function ($ip) {
163 11
            return ['ip_address' => $ip];
164 23
        }, $this->makeArrayOfIps($list));
165
    }
166
167
    /**
168
     * Make a list of arrays from all sort of things.
169
     *
170
     * @param $list
171
     *
172
     * @return array
173
     */
174 23
    private function makeArrayOfIps($list)
175
    {
176 23
        $list = $list ?: [];
177
178 23
        $ips = [];
179
180 23
        foreach ($list as $item) {
181 11
            $ips = array_merge($ips, $this->getIpsFromAnything($item));
182
        }
183
184 23
        return $ips;
185
    }
186
187
    /**
188
     * Get a list of ips from anything.
189
     *
190
     * @param $item
191
     *
192
     * @return array
193
     */
194 11
    private function getIpsFromAnything($item)
195
    {
196 11
        if (starts_with($item, 'country:')) {
197 1
            return [$item];
198
        }
199
200 10
        $item = $this->ipAddress()->hostToIp($item);
201
202 10
        if ($this->ipAddress()->ipV4Valid($item)) {
203 10
            return [$item];
204
        }
205
206
        return $this->readFile($item);
207
    }
208
209
    /**
210
     * Search for an ip in alist of ips.
211
     *
212
     * @param $ip
213
     * @param $ips
214
     *
215
     * @return null|\Illuminate\Database\Eloquent\Model
216
     */
217 22
    private function ipArraySearch($ip, $ips)
218
    {
219 22
        foreach ($ips as $key => $value) {
220
            if (
221 11
                (isset($value['ip_address']) && $value['ip_address'] == $ip) ||
222 8
                (strval($key) == $ip) ||
223 8
                ($value == $ip)
224
            ) {
225 8
                return $value;
226
            }
227
        }
228 22
    }
229
230
    /**
231
     * Get all IPs from database.
232
     *
233
     * @return \Illuminate\Support\Collection
234
     */
235 23
    private function getAllFromDatabase()
236
    {
237 23
        if ($this->config()->get('use_database')) {
238 10
            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 238 which is incompatible with the return type documented by PragmaRX\Firewall\Reposi...ist::getAllFromDatabase of type Illuminate\Support\Collection.
Loading history...
239
        } else {
240 13
            return collect([]);
241
        }
242
    }
243
244
    /**
245
     * Merge IP lists.
246
     *
247
     * @param $database_ips
248
     * @param $config_ips
249
     *
250
     * @return \Illuminate\Support\Collection
251
     */
252 23
    private function mergeLists($database_ips, $config_ips)
253
    {
254 23
        return collect($database_ips)
255 23
            ->merge(collect($config_ips));
256
    }
257
258
    /**
259
     * Check if an IP address is in a secondary (black/white) list.
260
     *
261
     * @param $ip_address
262
     *
263
     * @return bool|array
264
     */
265 22
    public function checkSecondaryLists($ip_address)
266
    {
267 22
        foreach ($this->all() as $range) {
268 11
            if ($this->ipAddress()->hostToIp($range) == $ip_address || $this->ipAddress()->validRange($ip_address, $range)) {
269 2
                return $range;
270
            }
271
        }
272
273 22
        return false;
274
    }
275
276
    /**
277
     * Add an IP to black or whitelist.
278
     *
279
     * @param $whitelist
280
     * @param $ip
281
     * @param bool $force
282
     *
283
     * @return bool
284
     */
285 22
    public function addToList($whitelist, $ip, $force = false)
286
    {
287 22
        $list = $whitelist
288 14
            ? 'whitelist'
289 22
            : 'blacklist';
290
291 22
        if (!$this->ipAddress()->isValid($ip)) {
292 2
            $this->messages()->addMessage(sprintf('%s is not a valid IP address', $ip));
293
294 2
            return false;
295
        }
296
297 20
        $listed = $this->whichList($ip);
298
299 20
        if ($listed == $list) {
300
            $this->messages()->addMessage(sprintf('%s is already %s', $ip, $list.'ed'));
301
302
            return false;
303
        } else {
304 20
            if (empty($listed) || $force) {
305 20
                if (!empty($listed)) {
306 2
                    $this->remove($ip);
307
                }
308
309 20
                $this->addToProperList($whitelist, $ip);
310
311 20
                $this->messages()->addMessage(sprintf('%s is now %s', $ip, $list.'ed'));
312
313 20
                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 22
    public function whichList($ip_address)
330
    {
331 22
        if (!$ip_found = $this->find($ip_address)) {
332 22
            if (!$ip_found = $this->findByCountry($ip_address)) {
333 22
                if (!$ip_found = $this->checkSecondaryLists($ip_address)) {
334 22
                    return;
335
                }
336
            }
337
        }
338
339 15
        return !is_null($ip_found)
340 15
            ? ($ip_found['whitelisted'] ? 'whitelist' : 'blacklist')
341 15
            : null;
342
    }
343
344
    /**
345
     * Remove IP from all lists.
346
     *
347
     * @param $ip
348
     *
349
     * @return bool
350
     */
351 4
    public function remove($ip)
352
    {
353 4
        $listed = $this->whichList($ip);
354
355 4
        if (!empty($listed)) {
356 4
            $this->delete($ip);
357
358 4
            $this->messages()->addMessage(sprintf('%s removed from %s', $ip, $listed));
359
360 4
            return true;
361
        }
362
363
        $this->messages()->addMessage(sprintf('%s is not listed', $ip));
364
365
        return false;
366
    }
367
368
    /**
369
     * Add ip or range to array list.
370
     *
371
     * @param $whitelist
372
     * @param $ip
373
     *
374
     * @return array|mixed
375
     */
376 11
    private function addToArrayList($whitelist, $ip)
377
    {
378 11
        $data = $this->config()->get($list = $whitelist ? 'whitelist' : 'blacklist');
379
380 11
        $data[] = $ip;
381
382 11
        $this->config()->set($list, $data);
383
384 11
        return $data;
385
    }
386
387
    /**
388
     * Find an IP address in the data source.
389
     *
390
     * @param string $ip
391
     *
392
     * @return mixed
393
     */
394 22
    public function find($ip)
395
    {
396 22
        if ($this->cache()->has($ip)) {
397
            return $this->cache()->get($ip);
398
        }
399
400 22
        if ($model = $this->findIp($ip)) {
401 15
            $this->cache()->remember($model);
402
        }
403
404 22
        return $model;
405
    }
406
407
    /**
408
     * Find an IP address by country.
409
     *
410
     * @param $country
411
     *
412
     * @return mixed
413
     */
414 22
    public function findByCountry($country)
415
    {
416 22
        if ($this->config()->get('enable_country_search') && !is_null($country = $this->countries()->makeCountryFromString($country))) {
417 2
            return $this->find($country);
418
        }
419 20
    }
420
421
    /**
422
     * Find a Ip in the data source.
423
     *
424
     * @param string $ip
425
     *
426
     * @return void
427
     */
428 20
    public function addToProperList($whitelist, $ip)
429
    {
430 20
        $this->config()->get('use_database') ?
431 9
            $this->addToDatabaseList($whitelist, $ip) :
432 11
            $this->addToArrayList($whitelist, $ip);
433 20
    }
434
435
    /**
436
     * Delete ip address.
437
     *
438
     * @param $ipAddress
439
     *
440
     * @return bool|void
441
     */
442 4
    public function delete($ipAddress)
443
    {
444 4
        $this->config()->get('use_database') ?
445 2
            $this->removeFromDatabaseList($ipAddress) :
446 2
            $this->removeFromArrayList($ipAddress);
447 4
    }
448
449
    /**
450
     * Get all IP addresses.
451
     *
452
     * @return \Illuminate\Support\Collection
453
     */
454 23
    public function all()
455
    {
456 23
        $cacheTime = $this->config()->get('ip_list_cache_expire_time');
457
458 23
        if ($cacheTime > 0 && $list = $this->cache()->get(static::IP_ADDRESS_LIST_CACHE_NAME)) {
459
            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...
460
        }
461
462 23
        $list = $this->mergeLists(
463 23
            $this->getAllFromDatabase(),
464 23
            $this->toModels($this->getNonDatabaseIps())
465
        );
466
467 23
        if ($cacheTime > 0) {
468
            $this->cache()->put(
469
                static::IP_ADDRESS_LIST_CACHE_NAME,
470
                $list,
471
                $cacheTime
472
            );
473
        }
474
475 23
        return $list;
476
    }
477
478
    /**
479
     * Find ip address in all lists.
480
     *
481
     * @param $ip
482
     *
483
     * @return \Illuminate\Database\Eloquent\Model|null|static
484
     */
485 22
    private function findIp($ip)
486
    {
487 22
        if ($model = $this->nonDatabaseFind($ip)) {
488 8
            return $model;
489
        }
490
491 22
        if ($this->config()->get('use_database')) {
492 10
            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...
493
        }
494 12
    }
495
496
    /**
497
     * Find ip in non database lists.
498
     *
499
     * @param $ip
500
     *
501
     * @return \Illuminate\Database\Eloquent\Model
502
     */
503 22
    private function nonDatabaseFind($ip)
504
    {
505 22
        $ips = $this->getNonDatabaseIps();
506
507 22
        if ($ip = $this->ipArraySearch($ip, $ips)) {
508 8
            return $this->makeModel($ip);
509
        }
510 22
    }
511
512
    /**
513
     * Add ip or range to database.
514
     *
515
     * @param $whitelist
516
     * @param $ip
517
     *
518
     * @return \Illuminate\Database\Eloquent\Model
519
     */
520 9
    private function addToDatabaseList($whitelist, $ip)
521
    {
522 9
        $this->model->unguard();
523
524 9
        $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...
525 9
            'ip_address'  => $ip,
526 9
            'whitelisted' => $whitelist,
527
        ]);
528
529 9
        $this->cache()->remember($model);
530
531 9
        return $model;
532
    }
533
}
534