Completed
Push — master ( 2bd6c2...9d13eb )
by Antonio Carlos
08:09 queued 01:00
created

DataRepository::makeArrayOfIps()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 5
nc 2
nop 1
dl 0
loc 11
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace PragmaRX\Firewall\Repositories;
4
5
use Exception;
6
use PragmaRX\Firewall\Repositories\Cache\Cache;
7
use PragmaRX\Firewall\Vendor\Laravel\Models\Firewall as FirewallModel;
8
use PragmaRX\Support\Config;
9
use PragmaRX\Support\Filesystem;
10
use PragmaRX\Support\IpAddress;
11
12
class DataRepository implements DataRepositoryInterface
13
{
14
    const IP_ADDRESS_LIST_CACHE_NAME = 'firewall.ip_address_list';
15
16
    /**
17
     * @var \PragmaRX\Firewall\Firewall
18
     */
19
    public $firewall;
20
21
    /**
22
     * @var \PragmaRX\Firewall\Repositories\Countries
23
     */
24
    public $countries;
25
26
    /**
27
     * @var FirewallModel
28
     */
29
    private $model;
30
31
    /**
32
     * @var Cache|CacheManager
0 ignored issues
show
Bug introduced by
The type PragmaRX\Firewall\Repositories\CacheManager was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
33
     */
34
    private $cache;
35
36
    /**
37
     * @var Config
38
     */
39
    private $config;
40
41
    /**
42
     * @var Filesystem
43
     */
44
    private $fileSystem;
45
46
    /**
47
     * @var Message
48
     */
49
    private $messageRepository;
50
51
    /**
52
     * Create instance of DataRepository.
53
     *
54
     * @param FirewallModel $model
55
     * @param Config        $config
56
     * @param CacheManager  $cache
57
     * @param Filesystem    $fileSystem
58
     * @param Countries     $countries
59
     * @param Message       $messageRepository
60
     */
61
    public function __construct(
62
        FirewallModel $model,
63
        Config $config,
64
        Cache $cache,
65
        Filesystem $fileSystem,
66
        Countries $countries,
67
        Message $messageRepository
68
    ) {
69
        $this->model = $model;
70
71
        $this->config = $config;
72
73
        $this->fileSystem = $fileSystem;
74
75
        $this->cache = $cache;
76
77
        $this->countries = $countries;
78
79
        $this->messageRepository = $messageRepository;
80
    }
81
82
    /**
83
     * Add ip or range to array list.
84
     *
85
     * @param $whitelist
86
     * @param $ip
87
     *
88
     * @return array|mixed
89
     */
90
    private function addToArrayList($whitelist, $ip)
91
    {
92
        $data = $this->config->get($list = $whitelist ? 'whitelist' : 'blacklist');
93
94
        $data[] = $ip;
95
96
        $this->config->set($list, $data);
97
98
        return $data;
99
    }
100
101
    /**
102
     * Add ip or range to database.
103
     *
104
     * @param $whitelist
105
     * @param $ip
106
     *
107
     * @return \Illuminate\Database\Eloquent\Model
108
     */
109
    private function addToDatabaseList($whitelist, $ip)
110
    {
111
        $this->model->unguard();
112
113
        $model = $this->model->create([
114
            'ip_address'  => $ip,
115
            'whitelisted' => $whitelist,
116
        ]);
117
118
        $this->cache->remember($model);
119
120
        return $model;
121
    }
122
123
    /**
124
     * Find an IP address in the data source.
125
     *
126
     * @param string $ip
127
     *
128
     * @return mixed
129
     */
130
    public function find($ip)
131
    {
132
        if ($this->cache->has($ip)) {
133
            return $this->cache->get($ip);
134
        }
135
136
        if ($model = $this->findIp($ip)) {
137
            $this->cache->remember($model);
138
        }
139
140
        return $model;
141
    }
142
143
    /**
144
     * Find an IP address by country.
145
     *
146
     * @param $country
147
     *
148
     * @return mixed
149
     */
150
    public function findByCountry($country)
151
    {
152
        if ($this->config->get('enable_country_search') && !is_null($country = $this->makeCountryFromString($country))) {
153
            return $this->find($country);
154
        }
155
    }
156
157
    /**
158
     * Make a country info from a string.
159
     *
160
     * @param $country
161
     *
162
     * @return string
163
     */
164
    public function makeCountryFromString($country)
165
    {
166
        if ($ips = IpAddress::isCidr($country)) {
167
            $country = $ips[0];
168
        }
169
170
        if ($this->validCountry($country)) {
171
            return $country;
172
        }
173
174
        if ($this->ipIsValid($country)) {
175
            $country = $this->countries->getCountryFromIp($country);
176
        }
177
178
        return "country:{$country}";
179
    }
180
181
    /**
182
     * Get country code from an IP address.
183
     *
184
     * @param $ip_address
185
     *
186
     * @return string
187
     */
188
    public function getCountryFromIp($ip_address)
189
    {
190
        return $this->countries->getCountryFromIp($ip_address);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->countries-...ntryFromIp($ip_address) also could return the type boolean which is incompatible with the documented return type string.
Loading history...
191
    }
192
193
    /**
194
     * Check if IP address is valid.
195
     *
196
     * @param $ip
197
     *
198
     * @return bool
199
     */
200
    public function ipIsValid($ip)
201
    {
202
        $ip = $this->hostToIp($ip);
203
204
        try {
205
            return IpAddress::ipV4Valid($ip) || $this->validCountry($ip);
206
        } catch (Exception $e) {
207
            return false;
208
        }
209
    }
210
211
    /**
212
     * Find a Ip in the data source.
213
     *
214
     * @param string $ip
215
     *
216
     * @return void
217
     */
218
    public function addToProperList($whitelist, $ip)
219
    {
220
        $this->config->get('use_database') ?
221
            $this->addToDatabaseList($whitelist, $ip) :
222
            $this->addToArrayList($whitelist, $ip);
223
    }
224
225
    /**
226
     * Delete ip address.
227
     *
228
     * @param $ipAddress
229
     *
230
     * @return bool|void
231
     */
232
    public function delete($ipAddress)
233
    {
234
        $this->config->get('use_database') ?
235
            $this->removeFromDatabaseList($ipAddress) :
236
            $this->removeFromArrayList($ipAddress);
237
    }
238
239
    /**
240
     * Get all IP addresses.
241
     *
242
     * @return \Illuminate\Support\Collection
243
     */
244
    public function all()
245
    {
246
        $cacheTime = $this->config->get('ip_list_cache_expire_time');
247
248
        if ($cacheTime && $list = $this->cache->get(static::IP_ADDRESS_LIST_CACHE_NAME)) {
249
            return $list;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $list returns the type Illuminate\Contracts\Cache\Repository which is incompatible with the documented return type Illuminate\Support\Collection.
Loading history...
250
        }
251
252
        $list = $this->mergeLists(
253
            $this->getAllFromDatabase(),
254
            $this->toModels($this->getNonDatabaseIps())
255
        );
256
257
        if ($cacheTime) {
258
            $this->cache->put(
259
                static::IP_ADDRESS_LIST_CACHE_NAME,
260
                $list,
261
                $this->config->get('ip_list_cache_expire_time')
262
            );
263
        }
264
265
        return $list;
266
    }
267
268
    /**
269
     * Get all IP addresses by country.
270
     *
271
     * @param $country
272
     *
273
     * @return \Illuminate\Support\Collection
274
     */
275
    public function allByCountry($country)
276
    {
277
        $country = $this->makeCountryFromString($country);
278
279
        return $this->all()->filter(function ($item) use ($country) {
280
            return $item['ip_address'] == $country ||
281
                $this->makeCountryFromString($this->getCountryFromIp($item['ip_address'])) == $country;
282
        });
283
    }
284
285
    /**
286
     * Clear all items from all lists.
287
     *
288
     * @return int
289
     */
290
    public function clear()
291
    {
292
        $deleted = 0;
293
294
        foreach ($this->all() as $ip) {
295
            if ($this->delete($ip['ip_address'])) {
296
                $deleted++;
297
            }
298
        }
299
300
        return $deleted;
301
    }
302
303
    /**
304
     * Find ip address in all lists.
305
     *
306
     * @param $ip
307
     *
308
     * @return \Illuminate\Database\Eloquent\Model|null|static
309
     */
310
    private function findIp($ip)
311
    {
312
        if ($model = $this->nonDatabaseFind($ip)) {
313
            return $model;
314
        }
315
316
        if ($this->config->get('use_database')) {
317
            return $this->model->where('ip_address', $ip)->first();
318
        }
319
    }
320
321
    /**
322
     * Find ip in non database lists.
323
     *
324
     * @param $ip
325
     *
326
     * @return \Illuminate\Database\Eloquent\Model
327
     */
328
    private function nonDatabaseFind($ip)
329
    {
330
        $ips = $this->getNonDatabaseIps();
331
332
        if ($ip = $this->ipArraySearch($ip, $ips)) {
333
            return $this->makeModel($ip);
334
        }
335
    }
336
337
    /**
338
     * Get a list of non database ip addresses.
339
     *
340
     * @return array
341
     */
342
    private function getNonDatabaseIps()
343
    {
344
        return array_merge_recursive(
345
            array_map(function ($ip) {
346
                $ip['whitelisted'] = true;
347
348
                return $ip;
349
            }, $this->formatIpArray($this->config->get('whitelist'))),
350
351
            array_map(function ($ip) {
352
                $ip['whitelisted'] = false;
353
354
                return $ip;
355
            }, $this->formatIpArray($this->config->get('blacklist')))
356
        );
357
    }
358
359
    /**
360
     * Remove ip from all array lists.
361
     *
362
     * @param $ipAddress
363
     *
364
     * @return bool
365
     */
366
    private function removeFromArrayList($ipAddress)
367
    {
368
        return $this->removeFromArrayListType('whitelist', $ipAddress) ||
369
            $this->removeFromArrayListType('blacklist', $ipAddress);
370
    }
371
372
    /**
373
     * Remove the ip address from an array list.
374
     *
375
     * @param $type
376
     * @param $ipAddress
377
     *
378
     * @return bool
379
     */
380
    private function removeFromArrayListType($type, $ipAddress)
381
    {
382
        if (($key = array_search($ipAddress, $data = $this->config->get($type))) !== false) {
383
            unset($data[$key]);
384
385
            $this->config->set($type, $data);
386
387
            return true;
388
        }
389
390
        return false;
391
    }
392
393
    /**
394
     * Remove ip from database.
395
     *
396
     * @param $ipAddress
397
     *
398
     * @return bool
399
     */
400
    private function removeFromDatabaseList($ipAddress)
401
    {
402
        if ($ip = $this->find($ipAddress)) {
403
            $ip->delete();
0 ignored issues
show
Bug introduced by
The call to PragmaRX\Firewall\Reposi...ataRepository::delete() has too few arguments starting with ipAddress. ( Ignorable by Annotation )

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

403
            /** @scrutinizer ignore-call */ $ip->delete();

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
404
405
            $this->cache->forget($ipAddress);
406
407
            return true;
408
        }
409
410
        return false;
411
    }
412
413
    /**
414
     * Transform a list of ips to a list of models.
415
     *
416
     * @param $ipList
417
     *
418
     * @return \Illuminate\Support\Collection
419
     */
420
    private function toModels($ipList)
421
    {
422
        $ips = [];
423
424
        foreach ($ipList as $ip) {
425
            $ips[] = $this->makeModel($ip);
426
        }
427
428
        return collect($ips);
429
    }
430
431
    /**
432
     * Make a model instance.
433
     *
434
     * @param $ip
435
     *
436
     * @return \Illuminate\Database\Eloquent\Model
437
     */
438
    private function makeModel($ip)
439
    {
440
        return $this->model->newInstance($ip);
441
    }
442
443
    /**
444
     * Read a file contents.
445
     *
446
     * @param $file
447
     *
448
     * @return array
449
     */
450
    private function readFile($file)
451
    {
452
        if ($this->fileSystem->exists($file)) {
453
            $lines = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
454
455
            return $this->makeArrayOfIps($lines);
456
        }
457
458
        return [];
459
    }
460
461
    /**
462
     * Format all ips in an array.
463
     *
464
     * @param $list
465
     *
466
     * @return array
467
     */
468
    private function formatIpArray($list)
469
    {
470
        return array_map(function ($ip) {
471
            return ['ip_address' => $ip];
472
        }, $this->makeArrayOfIps($list));
473
    }
474
475
    /**
476
     * Make a list of arrays from all sort of things.
477
     *
478
     * @param $list
479
     *
480
     * @return array
481
     */
482
    private function makeArrayOfIps($list)
483
    {
484
        $list = $list ?: [];
485
486
        $ips = [];
487
488
        foreach ($list as $item) {
489
            $ips = array_merge($ips, $this->getIpsFromAnything($item));
490
        }
491
492
        return $ips;
493
    }
494
495
    /**
496
     * Get a list of ips from anything.
497
     *
498
     * @param $item
499
     *
500
     * @return array
501
     */
502
    private function getIpsFromAnything($item)
503
    {
504
        if (starts_with($item, 'country:')) {
505
            return [$item];
506
        }
507
508
        $item = $this->hostToIp($item);
509
510
        if (IpAddress::ipV4Valid($item)) {
511
            return [$item];
512
        }
513
514
        return $this->readFile($item);
515
    }
516
517
    /**
518
     * Search for an ip in alist of ips.
519
     *
520
     * @param $ip
521
     * @param $ips
522
     *
523
     * @return null|\Illuminate\Database\Eloquent\Model
524
     */
525
    private function ipArraySearch($ip, $ips)
526
    {
527
        foreach ($ips as $key => $value) {
528
            if (
529
                (isset($value['ip_address']) && $value['ip_address'] == $ip) ||
530
                (strval($key) == $ip) ||
531
                ($value == $ip)
532
            ) {
533
                return $value;
534
            }
535
        }
536
    }
537
538
    /**
539
     * Get all IPs from database.
540
     *
541
     * @return \Illuminate\Support\Collection
542
     */
543
    private function getAllFromDatabase()
544
    {
545
        if ($this->config->get('use_database')) {
546
            return $this->model->all();
547
        } else {
548
            return collect([]);
549
        }
550
    }
551
552
    /**
553
     * Merge IP lists.
554
     *
555
     * @param $database_ips
556
     * @param $config_ips
557
     *
558
     * @return \Illuminate\Support\Collection
559
     */
560
    private function mergeLists($database_ips, $config_ips)
561
    {
562
        return collect($database_ips)
563
            ->merge(collect($config_ips));
564
    }
565
566
    /**
567
     * Get the ip address of a host.
568
     *
569
     * @param $ip
570
     *
571
     * @return string
572
     */
573
    public function hostToIp($ip)
574
    {
575
        if (is_string($ip) && starts_with($ip, $string = 'host:')) {
576
            return gethostbyname(str_replace($string, '', $ip));
577
        }
578
579
        return $ip;
580
    }
581
582
    /**
583
     * Check if an IP address is in a secondary (black/white) list.
584
     *
585
     * @param $ip_address
586
     *
587
     * @return bool|array
588
     */
589
    public function checkSecondaryLists($ip_address)
590
    {
591
        foreach ($this->all() as $range) {
592
            if ($this->hostToIp($range) == $ip_address || $this->ipIsInValidRange($ip_address, $range)) {
593
                return $range;
594
            }
595
        }
596
597
        return false;
598
    }
599
600
    /**
601
     * Check if IP is in a valid range.
602
     *
603
     * @param $ip_address
604
     * @param $range
605
     *
606
     * @return bool
607
     */
608
    private function ipIsInValidRange($ip_address, $range)
609
    {
610
        return $this->config->get('enable_range_search') &&
611
            IpAddress::ipV4Valid($range->ip_address) &&
612
            IpAddress::ipv4InRange($ip_address, $range->ip_address);
613
    }
614
615
    /**
616
     * Check if a string is a valid country info.
617
     *
618
     * @param $country
619
     *
620
     * @return bool
621
     */
622
    public function validCountry($country)
623
    {
624
        $country = strtolower($country);
625
626
        if ($this->config->get('enable_country_search')) {
627
            if (starts_with($country, 'country:') && $this->countries->isValid($country)) {
628
                return true;
629
            }
630
        }
631
632
        return false;
633
    }
634
635
    /**
636
     * Add an IP to black or whitelist.
637
     *
638
     * @param $whitelist
639
     * @param $ip
640
     * @param bool $force
641
     *
642
     * @return bool
643
     */
644
    public function addToList($whitelist, $ip, $force = false)
645
    {
646
        $list = $whitelist
647
            ? 'whitelist'
648
            : 'blacklist';
649
650
        if (!$this->ipIsValid($ip)) {
651
            $this->messageRepository->addMessage(sprintf('%s is not a valid IP address', $ip));
652
653
            return false;
654
        }
655
656
        $listed = $this->whichList($ip);
657
658
        if ($listed == $list) {
659
            $this->messageRepository->addMessage(sprintf('%s is already %s', $ip, $list.'ed'));
660
661
            return false;
662
        } else {
663
            if (empty($listed) || $force) {
664
                if (!empty($listed)) {
665
                    $this->remove($ip);
666
                }
667
668
                $this->addToProperList($whitelist, $ip);
669
670
                $this->messageRepository->addMessage(sprintf('%s is now %s', $ip, $list.'ed'));
671
672
                return true;
673
            }
674
        }
675
676
        $this->messageRepository->addMessage(sprintf('%s is currently %sed', $ip, $listed));
677
678
        return false;
679
    }
680
681
    /**
682
     * Tell in which list (black/white) an IP address is.
683
     *
684
     * @param $ip_address
685
     *
686
     * @return null|string
687
     */
688
    public function whichList($ip_address)
689
    {
690
        if (!$ip_found = $this->find($ip_address)) {
691
            if (!$ip_found = $this->findByCountry($ip_address)) {
692
                if (!$ip_found = $this->checkSecondaryLists($ip_address)) {
693
                    return;
694
                }
695
            }
696
        }
697
698
        return !is_null($ip_found)
699
            ? ($ip_found['whitelisted'] ? 'whitelist' : 'blacklist')
700
            : null;
701
    }
702
703
    /**
704
     * Remove IP from all lists.
705
     *
706
     * @param $ip
707
     *
708
     * @return bool
709
     */
710
    public function remove($ip)
711
    {
712
        $listed = $this->whichList($ip);
713
714
        if (!empty($listed)) {
715
            $this->delete($ip);
716
717
            $this->messageRepository->addMessage(sprintf('%s removed from %s', $ip, $listed));
718
719
            return true;
720
        }
721
722
        $this->messageRepository->addMessage(sprintf('%s is not listed', $ip));
723
724
        return false;
725
    }
726
727
    /**
728
     * Get the GeoIP instance.
729
     *
730
     * @return object
731
     */
732
    public function getGeoIp()
733
    {
734
        return $this->countries->getGeoIp();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->countries->getGeoIp() returns the type PragmaRX\Support\GeoIp\GeoIp which is incompatible with the documented return type object.
Loading history...
735
    }
736
}
737