Passed
Push — master ( 47d82d...fb5b6d )
by Antonio Carlos
02:29
created

DataRepository::toCollection()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace PragmaRX\Firewall\Repositories;
4
5
/*
6
 * Part of the Firewall package.
7
 *
8
 * NOTICE OF LICENSE
9
 *
10
 * Licensed under the 3-clause BSD License.
11
 *
12
 * This source file is subject to the 3-clause BSD License that is
13
 * bundled with this package in the LICENSE file.  It is also available at
14
 * the following URL: http://www.opensource.org/licenses/BSD-3-Clause
15
 *
16
 * @package    Firewall
17
 * @author     Antonio Carlos Ribeiro @ PragmaRX
18
 * @license    BSD License (3-clause)
19
 * @copyright  (c) 2013, PragmaRX
20
 * @link       http://pragmarx.com
21
 */
22
23
use PragmaRX\Firewall\Vendor\Laravel\Models\Firewall;
24
use PragmaRX\Support\CacheManager;
25
use PragmaRX\Support\Config;
26
use PragmaRX\Support\Filesystem;
27
use PragmaRX\Support\IpAddress;
28
use ReflectionClass;
29
30
class DataRepository implements DataRepositoryInterface
31
{
32
    const CACHE_BASE_NAME = 'firewall.';
33
34
    const IP_ADDRESS_LIST_CACHE_NAME = 'firewall.ip_address_list';
35
36
    /**
37
     * @var object
38
     */
39
    public $firewall;
40
41
    /**
42
     * @var object
43
     */
44
    public $countries;
45
46
    /**
47
     * @var object
48
     */
49
    private $model;
50
51
    /**
52
     * @var Cache|CacheManager
53
     */
54
    private $cache;
55
56
    /**
57
     * @var Config
58
     */
59
    private $config;
60
61
    /**
62
     * @var Filesystem
63
     */
64
    private $fileSystem;
65
66
    /**
67
     * @var Message
68
     */
69
    private $messageRepository;
70
71
    /**
72
     * Create instance of DataRepository.
73
     *
74
     * @param Firewall     $model
75
     * @param Config       $config
76
     * @param CacheManager $cache
77
     * @param Filesystem   $fileSystem
78
     * @param Countries    $countries
79
     */
80
    public function __construct(
81
        Firewall $model,
82
        Config $config,
83
        CacheManager $cache,
84
        Filesystem $fileSystem,
85
        Countries $countries,
86
        Message $messageRepository
87
    ) {
88
        $this->model = $model;
0 ignored issues
show
Documentation Bug introduced by
It seems like $model of type PragmaRX\Firewall\Vendor\Laravel\Models\Firewall is incompatible with the declared type object of property $model.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
89
90
        $this->config = $config;
91
92
        $this->fileSystem = $fileSystem;
93
94
        $this->cache = $cache;
95
96
        $this->countries = $countries;
0 ignored issues
show
Documentation Bug introduced by
It seems like $countries of type PragmaRX\Firewall\Repositories\Countries is incompatible with the declared type object of property $countries.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
97
98
        $this->messageRepository = $messageRepository;
99
    }
100
101
    /**
102
     * Add ip or range to array list.
103
     *
104
     * @param $whitelist
105
     * @param $ip
106
     *
107
     * @return array|mixed
108
     */
109
    private function addToArrayList($whitelist, $ip)
110
    {
111
        $data = $this->config->get($list = $whitelist ? 'whitelist' : 'blacklist');
112
113
        $data[] = $ip;
114
115
        $this->config->set($list, $data);
116
117
        return $data;
118
    }
119
120
    /**
121
     * Add ip or range to database.
122
     *
123
     * @param $whitelist
124
     * @param $ip
125
     *
126
     * @return \Illuminate\Database\Eloquent\Model
127
     */
128
    private function addToDatabaseList($whitelist, $ip)
129
    {
130
        $this->model->unguard();
131
132
        $model = $this->model->create([
133
            'ip_address'  => $ip,
134
            'whitelisted' => $whitelist,
135
        ]);
136
137
        $this->cacheRemember($model);
138
139
        return $model;
140
    }
141
142
    /**
143
     * Find an IP address in the data source.
144
     *
145
     * @param string $ip
146
     *
147
     * @return \Illuminate\Database\Eloquent\Model
148
     */
149
    public function find($ip)
150
    {
151
        if ($this->cacheHas($ip)) {
152
            return $this->cacheGet($ip);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->cacheGet($ip) returns the type Illuminate\Contracts\Cache\Repository which is incompatible with the documented return type Illuminate\Database\Eloquent\Model.
Loading history...
153
        }
154
155
        if ($model = $this->findIp($ip)) {
156
            $this->cacheRemember($model);
157
        }
158
159
        return $model;
160
    }
161
162
    /**
163
     * Find an IP address by country.
164
     *
165
     * @param $country
166
     *
167
     * @return \Illuminate\Database\Eloquent\Model|null
168
     */
169
    public function findByCountry($country)
170
    {
171
        if ($this->config->get('enable_country_search') && !is_null($country = $this->makeCountryFromString($country))) {
172
            return $this->find($country);
173
        }
174
175
        return null;
176
    }
177
178
    /**
179
     * Make a country info from a string.
180
     *
181
     * @param $country
182
     *
183
     * @return string
184
     */
185
    public function makeCountryFromString($country)
186
    {
187
        if ($ips = IpAddress::isCidr($country)) {
188
            $country = $ips[0];
189
        }
190
191
        if ($this->validCountry($country)) {
192
            return $country;
193
        }
194
195
        if ($this->ipIsValid($country)) {
196
            $country = $this->countries->getCountryFromIp($country);
197
        }
198
199
        return "country:{$country}";
200
    }
201
202
    /**
203
     * Get country code from an IP address.
204
     *
205
     * @param $ip_address
206
     *
207
     * @return string
208
     */
209
    public function getCountryFromIp($ip_address)
210
    {
211
        return $this->countries->getCountryFromIp($ip_address);
212
    }
213
214
    /**
215
     * Check if IP address is valid.
216
     *
217
     * @param $ip
218
     *
219
     * @return bool
220
     */
221
    public function ipIsValid($ip)
222
    {
223
        $ip = $this->hostToIp($ip);
224
225
        try {
226
            return IpAddress::ipV4Valid($ip) || $this->validCountry($ip);
227
        } catch (Exception $e) {
0 ignored issues
show
Bug introduced by
The type PragmaRX\Firewall\Repositories\Exception was not found. Did you mean Exception? If so, make sure to prefix the type with \.
Loading history...
228
            return false;
229
        }
230
    }
231
232
    /**
233
     * Find a Ip in the data source.
234
     *
235
     * @param string $ip
236
     */
237
    public function addToProperList($whitelist, $ip)
238
    {
239
        $this->config->get('use_database') ?
240
            $this->addToDatabaseList($whitelist, $ip) :
241
            $this->addToArrayList($whitelist, $ip);
242
    }
243
244
    /**
245
     * Delete ip address.
246
     *
247
     * @param $ipAddress
248
     * @return bool|void
249
     */
250
    public function delete($ipAddress)
251
    {
252
        $this->config->get('use_database') ?
253
            $this->removeFromDatabaseList($ipAddress) :
254
            $this->removeFromArrayList($ipAddress);
255
    }
256
257
    public function cacheKey($ip)
258
    {
259
        return static::CACHE_BASE_NAME."ip_address.$ip";
260
    }
261
262
    public function cacheHas($ip)
263
    {
264
        if ($this->config->get('cache_expire_time')) {
265
            return $this->cache->has($this->cacheKey($ip));
266
        }
267
268
        return false;
269
    }
270
271
    public function cacheGet($ip)
272
    {
273
        return $this->cache->get($this->cacheKey($ip));
274
    }
275
276
    public function cacheForget($ip)
277
    {
278
        $this->cache->forget($this->cacheKey($ip));
279
    }
280
281
    public function cacheRemember($model)
282
    {
283
        if ($timeout = $this->config->get('cache_expire_time')) {
284
            $this->cache->put($this->cacheKey($model->ip_address), $model, $timeout);
285
        }
286
    }
287
288
    /**
289
     * Get all IP addresses.
290
     *
291
     * @return \Illuminate\Support\Collection
292
     */
293
    public function all()
294
    {
295
        $cacheTime = $this->config->get('ip_list_cache_expire_time');
296
297
        if ($cacheTime && $list = $this->cache->get(static::IP_ADDRESS_LIST_CACHE_NAME)) {
298
            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...
299
        }
300
301
        $list = $this->mergeLists(
302
            $this->getAllFromDatabase(),
303
            $this->toModels($this->getNonDatabaseIps())
304
        );
305
306
        if ($cacheTime) {
307
            $this->cache->put(
308
                static::IP_ADDRESS_LIST_CACHE_NAME,
309
                $list,
310
                $this->config->get('ip_list_cache_expire_time')
311
            );
312
        }
313
314
        return $list;
315
    }
316
317
    /**
318
     * Get all IP addresses by country.
319
     *
320
     * @param $country
321
     *
322
     * @return \Illuminate\Support\Collection
323
     */
324
    public function allByCountry($country)
325
    {
326
        $country = $this->makeCountryFromString($country);
327
328
        return $this->all()->filter(function ($item) use ($country) {
329
            return $item['ip_address'] == $country ||
330
                $this->makeCountryFromString($this->getCountryFromIp($item['ip_address'])) == $country;
331
        });
332
    }
333
334
    /**
335
     * Clear all items from all lists.
336
     *
337
     * @return int
338
     */
339
    public function clear()
340
    {
341
        /**
342
         * Deletes one by one to also remove them from cache.
343
         */
344
        $deleted = 0;
345
346
        foreach ($this->all() as $ip) {
347
            if ($this->delete($ip['ip_address'])) {
348
                $deleted++;
349
            }
350
        }
351
352
        return $deleted;
353
    }
354
355
    private function findIp($ip)
356
    {
357
        if ($model = $this->nonDatabaseFind($ip)) {
358
            return $model;
359
        }
360
361
        if ($this->config->get('use_database')) {
362
            return $this->model->where('ip_address', $ip)->first();
363
        }
364
    }
365
366
    private function nonDatabaseFind($ip)
367
    {
368
        $ips = $this->getNonDatabaseIps();
369
370
        if ($ip = $this->ipArraySearch($ip, $ips)) {
371
            return $this->makeModel($ip);
372
        }
373
    }
374
375
    private function getNonDatabaseIps()
376
    {
377
        return array_merge_recursive(
378
            array_map(function ($ip) {
379
                $ip['whitelisted'] = true;
380
381
                return $ip;
382
            }, $this->formatIpArray($this->config->get('whitelist'))),
383
384
            array_map(function ($ip) {
385
                $ip['whitelisted'] = false;
386
387
                return $ip;
388
            }, $this->formatIpArray($this->config->get('blacklist')))
389
        );
390
    }
391
392
    /**
393
     * Remove ip from all array lists.
394
     *
395
     * @param $ipAddress
396
     *
397
     * @return bool
398
     */
399
    private function removeFromArrayList($ipAddress)
400
    {
401
        return $this->removeFromArrayListType('whitelist', $ipAddress) ||
402
            $this->removeFromArrayListType('blacklist', $ipAddress);
403
    }
404
405
    /**
406
     * Remove the ip address from an array list.
407
     *
408
     * @param $type
409
     * @param $ipAddress
410
     * @return bool
411
     */
412
    private function removeFromArrayListType($type, $ipAddress)
413
    {
414
        if (($key = array_search($ipAddress, $data = $this->config->get($type))) !== false) {
415
            unset($data[$key]);
416
417
            $this->config->set($type, $data);
418
419
            return true;
420
        }
421
422
        return false;
423
    }
424
425
    /**
426
     * Remove ip from database.
427
     *
428
     * @param $ipAddress
429
     *
430
     * @return bool
431
     */
432
    private function removeFromDatabaseList($ipAddress)
433
    {
434
        if ($ip = $this->find($ipAddress)) {
435
            $ip->delete();
436
437
            $this->cacheForget($ipAddress);
438
439
            return true;
440
        }
441
442
        return false;
443
    }
444
445
    /**
446
     * Transform a list of ips to a list of models.
447
     *
448
     * @param $ipList
449
     *
450
     * @return \Illuminate\Support\Collection
451
     */
452
    private function toModels($ipList)
453
    {
454
        $ips = [];
455
456
        foreach ($ipList as $ip) {
457
            $ips[] = $this->makeModel($ip);
458
        }
459
460
        return collect($ips);
461
    }
462
463
    /**
464
     * Make a model instance.
465
     *
466
     * @param $ip
467
     *
468
     * @return \Illuminate\Database\Eloquent\Model
469
     */
470
    private function makeModel($ip)
471
    {
472
        return $this->model->newInstance($ip);
473
    }
474
475
    /**
476
     * Read a file contents.
477
     *
478
     * @param $file
479
     * @return array
480
     */
481
    private function readFile($file)
482
    {
483
        if ($this->fileSystem->exists($file)) {
484
            $lines = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
485
486
            return $this->makeArrayOfIps($lines);
487
        }
488
489
        return [];
490
    }
491
492
    /**
493
     * Format all ips in an array.
494
     *
495
     * @param $list
496
     * @return array
497
     */
498
    private function formatIpArray($list)
499
    {
500
        return array_map(function ($ip) {
501
            return ['ip_address' => $ip];
502
        }, $this->makeArrayOfIps($list));
503
    }
504
505
    /**
506
     * Make a list of arrays from all sort of things.
507
     *
508
     * @param $list
509
     * @return array
510
     */
511
    private function makeArrayOfIps($list)
512
    {
513
        $list = $list ?: [];
514
515
        $ips = [];
516
517
        foreach ($list as $item) {
518
            $ips = array_merge($ips, $this->getIpsFromAnything($item));
519
        }
520
521
        return $ips;
522
    }
523
524
    /**
525
     * Get a list of ips from anything.
526
     *
527
     * @param $item
528
     * @return array
529
     */
530
    private function getIpsFromAnything($item)
531
    {
532
        if (starts_with($item, 'country:')) {
533
            return [$item];
534
        }
535
536
        $item = $this->hostToIp($item);
537
538
        if (IpAddress::ipV4Valid($item)) {
539
            return [$item];
540
        }
541
542
        return $this->readFile($item);
543
    }
544
545
    /**
546
     * Search for an ip in alist of ips.
547
     *
548
     * @param $ip
549
     * @param $ips
550
     * @return null|\Illuminate\Database\Eloquent\Model
551
     */
552
    private function ipArraySearch($ip, $ips)
553
    {
554
        foreach ($ips as $key => $value) {
555
            if (
556
                (isset($value['ip_address']) && $value['ip_address'] == $ip) ||
557
                (strval($key) == $ip) ||
558
                ($value == $ip)
559
            ) {
560
                return $value;
561
            }
562
        }
563
564
        return null;
565
    }
566
567
    /**
568
     * Get all IPs from database.
569
     *
570
     * @return \Illuminate\Support\Collection
571
     */
572
    private function getAllFromDatabase()
573
    {
574
        if ($this->config->get('use_database')) {
575
            return $this->model->all();
576
        } else {
577
            return collect([]);
578
        }
579
    }
580
581
    /**
582
     * Merge IP lists.
583
     *
584
     * @param $database_ips
585
     * @param $config_ips
586
     *
587
     * @return \Illuminate\Support\Collection
588
     */
589
    private function mergeLists($database_ips, $config_ips)
590
    {
591
        return collect($database_ips)
592
            ->merge(collect($config_ips));
593
    }
594
595
    /**
596
     * Get the ip address of a host.
597
     *
598
     * @param $ip
599
     * @return string
600
     */
601
    public function hostToIp($ip)
602
    {
603
        if (is_string($ip) && starts_with($ip, $string = 'host:')) {
604
            return gethostbyname(str_replace($string, '', $ip));
605
        }
606
607
        return $ip;
608
    }
609
610
    /**
611
     * Check if an IP address is in a secondary (black/white) list.
612
     *
613
     * @param $ip_address
614
     *
615
     * @return bool|array
616
     */
617
    public function checkSecondaryLists($ip_address)
618
    {
619
        foreach ($this->all() as $range) {
620
            if ($this->hostToIp($range) == $ip_address || $this->ipIsInValidRange($ip_address, $range)) {
621
                return $range;
622
            }
623
        }
624
625
        return false;
626
    }
627
628
    /**
629
     * Check if IP is in a valid range.
630
     *
631
     * @param $ip_address
632
     * @param $range
633
     *
634
     * @return bool
635
     */
636
    private function ipIsInValidRange($ip_address, $range)
637
    {
638
        return $this->config->get('enable_range_search') &&
639
            IpAddress::ipV4Valid($range->ip_address) &&
640
            ipv4_in_range($ip_address, $range->ip_address);
0 ignored issues
show
Bug introduced by
The function ipv4_in_range was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

640
            /** @scrutinizer ignore-call */ ipv4_in_range($ip_address, $range->ip_address);
Loading history...
641
    }
642
643
    /**
644
     * Check if a string is a valid country info.
645
     *
646
     * @param $country
647
     *
648
     * @return bool
649
     */
650
    public function validCountry($country)
651
    {
652
        $country = strtolower($country);
653
654
        if ($this->config->get('enable_country_search')) {
655
            if (starts_with($country, 'country:') && $this->countries->isValid($country)) {
656
                return true;
657
            }
658
        }
659
660
        return false;
661
    }
662
663
    /**
664
     * Add an IP to black or whitelist.
665
     *
666
     * @param $whitelist
667
     * @param $ip
668
     * @param $force
669
     *
670
     * @return bool
671
     */
672
    public function addToList($whitelist, $ip, $force)
673
    {
674
        $list = $whitelist
675
            ? 'whitelist'
676
            : 'blacklist';
677
678
        if (!$this->ipIsValid($ip)) {
679
            $this->messageRepository->addMessage(sprintf('%s is not a valid IP address', $ip));
680
681
            return false;
682
        }
683
684
        $listed = $this->whichList($ip);
685
686
        if ($listed == $list) {
687
            $this->messageRepository->addMessage(sprintf('%s is already %s', $ip, $list.'ed'));
688
689
            return false;
690
        } else {
691
            if (!$listed || $force) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $listed of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
692
                if ($listed) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $listed of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
693
                    $this->remove($ip);
694
                }
695
696
                $this->addToProperList($whitelist, $ip);
697
698
                $this->messageRepository->addMessage(sprintf('%s is now %s', $ip, $list.'ed'));
699
700
                return true;
701
            }
702
        }
703
704
        $this->messageRepository->addMessage(sprintf('%s is currently %sed', $ip, $listed));
705
706
        return false;
707
    }
708
709
    /**
710
     * Tell in which list (black/white) an IP address is.
711
     *
712
     * @param $ip_address
713
     *
714
     * @return null|string
715
     */
716
    public function whichList($ip_address)
717
    {
718
        $ip_address = $ip_address
719
            ?: $this->getIp();
0 ignored issues
show
Bug introduced by
The method getIp() does not exist on PragmaRX\Firewall\Repositories\DataRepository. Did you maybe mean getGeoIp()? ( Ignorable by Annotation )

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

719
            ?: $this->/** @scrutinizer ignore-call */ getIp();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
720
721
        if (!$ip_found = $this->find($ip_address)) {
722
            if (!$ip_found = $this->findByCountry($ip_address)) {
723
                if (!$ip_found = $this->checkSecondaryLists($ip_address)) {
724
                    return null;
725
                }
726
            }
727
        }
728
729
        if ($ip_found) {
730
            return $ip_found['whitelisted']
731
                ? 'whitelist'
732
                : 'blacklist';
733
        }
734
735
        return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type null|string.
Loading history...
736
    }
737
738
    /**
739
     * Remove IP from all lists.
740
     *
741
     * @param $ip
742
     *
743
     * @return bool
744
     */
745
    public function remove($ip)
746
    {
747
        $listed = $this->whichList($ip);
748
749
        if ($listed) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $listed of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
750
            $this->delete($ip);
751
752
            $this->messageRepository->addMessage(sprintf('%s removed from %s', $ip, $listed));
753
754
            return true;
755
        }
756
757
        $this->messageRepository->addMessage(sprintf('%s is not listed', $ip));
758
759
        return false;
760
    }
761
762
    /**
763
     * Get the GeoIP instance.
764
     *
765
     * @return object
766
     */
767
    public function getGeoIp()
768
    {
769
        return $this->countries->getGeoIp();
770
    }
771
}
772