Passed
Push — 2.x ( 154feb...ed3db8 )
by Terry
02:04
created

Kernel::setTemplateDirectory()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 1
dl 0
loc 6
rs 10
c 0
b 0
f 0
1
<?php
2
/*
3
 * @name        Shieldon Firewall
4
 * @author      Terry Lin
5
 * @link        https://github.com/terrylinooo/shieldon
6
 * @package     Shieldon
7
 * @since       1.0.0
8
 * @version     2.0.0
9
 * @license     MIT
10
 *
11
 * Permission is hereby granted, free of charge, to any person obtaining a copy
12
 * of this software and associated documentation files (the "Software"), to deal
13
 * in the Software without restriction, including without limitation the rights
14
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
 * copies of the Software, and to permit persons to whom the Software is
16
 * furnished to do so, subject to the following conditions:
17
 *
18
 * The above copyright notice and this permission notice shall be included in
19
 * all copies or substantial portions of the Software.
20
 *
21
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
27
 * THE SOFTWARE.
28
 */
29
30
declare(strict_types=1);
31
32
namespace Shieldon\Firewall;
33
34
use Psr\Http\Message\ServerRequestInterface;
35
use Psr\Http\Message\ResponseInterface;
36
use Shieldon\Firewall\Captcha\CaptchaInterface;
37
use Shieldon\Firewall\Captcha\Foundation;
38
use Shieldon\Firewall\Component\ComponentInterface;
39
use Shieldon\Firewall\Component\ComponentProvider;
40
use Shieldon\Firewall\Driver\DriverProvider;
41
use Shieldon\Firewall\Helpers;
42
use Shieldon\Firewall\HttpFactory;
43
use Shieldon\Firewall\Log\ActionLogger;
44
use Shieldon\Firewall\Utils\Container;
45
use Shieldon\Firewall\IpTrait;
46
use Shieldon\Firewall\Kernel\FilterTrait;
47
use Shieldon\Firewall\Kernel\RuleTrait;
48
use Shieldon\Firewall\Kernel\LimitSessionTrait;
49
use Shieldon\Messenger\Messenger\MessengerInterface;
50
use function Shieldon\Firewall\__;
51
use function Shieldon\Firewall\get_cpu_usage;
52
use function Shieldon\Firewall\get_default_properties;
53
use function Shieldon\Firewall\get_memory_usage;
54
use function Shieldon\Firewall\get_request;
55
use function Shieldon\Firewall\get_response;
56
use function Shieldon\Firewall\get_session;
57
58
59
use Closure;
60
use InvalidArgumentException;
61
use LogicException;
62
use RuntimeException;
63
use function file_exists;
64
use function file_put_contents;
65
use function filter_var;
66
use function get_class;
67
use function gethostbyaddr;
68
use function is_dir;
69
use function is_writable;
70
use function microtime;
71
use function ob_end_clean;
72
use function ob_get_contents;
73
use function ob_start;
74
use function str_replace;
75
use function strpos;
76
use function strrpos;
77
use function substr;
78
use function time;
79
80
/**
81
 * The primary Shiendon class.
82
 */
83
class Kernel
84
{
85
    use IpTrait;
86
    use FilterTrait;
87
    use RuleTrait;
88
    use LimitSessionTrait;
89
90
    // Reason codes (allow)
91
    const REASON_IS_SEARCH_ENGINE = 100;
92
    const REASON_IS_GOOGLE = 101;
93
    const REASON_IS_BING = 102;
94
    const REASON_IS_YAHOO = 103;
95
    const REASON_IS_SOCIAL_NETWORK = 110;
96
    const REASON_IS_FACEBOOK = 111;
97
    const REASON_IS_TWITTER = 112;
98
99
    // Reason codes (deny)
100
    const REASON_TOO_MANY_SESSIONS = 1;
101
    const REASON_TOO_MANY_ACCESSES = 2; // (not used)
102
    const REASON_EMPTY_JS_COOKIE = 3;
103
    const REASON_EMPTY_REFERER = 4;
104
    
105
    const REASON_REACHED_LIMIT_DAY = 11;
106
    const REASON_REACHED_LIMIT_HOUR = 12;
107
    const REASON_REACHED_LIMIT_MINUTE = 13;
108
    const REASON_REACHED_LIMIT_SECOND = 14;
109
110
    const REASON_INVALID_IP = 40;
111
    const REASON_DENY_IP = 41;
112
    const REASON_ALLOW_IP = 42;
113
114
    const REASON_COMPONENT_IP = 81;
115
    const REASON_COMPONENT_RDNS = 82;
116
    const REASON_COMPONENT_HEADER = 83;
117
    const REASON_COMPONENT_USERAGENT = 84;
118
    const REASON_COMPONENT_TRUSTED_ROBOT = 85;
119
120
    const REASON_MANUAL_BAN = 99;
121
122
    // Action codes
123
    const ACTION_DENY = 0;
124
    const ACTION_ALLOW = 1;
125
    const ACTION_TEMPORARILY_DENY = 2;
126
    const ACTION_UNBAN = 9;
127
128
    // Result codes
129
    const RESPONSE_DENY = 0;
130
    const RESPONSE_ALLOW = 1;
131
    const RESPONSE_TEMPORARILY_DENY = 2;
132
    const RESPONSE_LIMIT_SESSION = 3;
133
134
    const LOG_LIMIT = 3;
135
    const LOG_PAGEVIEW = 11;
136
    const LOG_BLACKLIST = 98;
137
    const LOG_CAPTCHA = 99;
138
139
    const KERNEL_DIR = __DIR__;
140
141
    /**
142
     * Driver for storing data.
143
     *
144
     * @var \Shieldon\Firewall\Driver\DriverProvider
145
     */
146
    public $driver;
147
148
    /**
149
     * Container for Shieldon components.
150
     *
151
     * @var array
152
     */
153
    public $component = [];
154
155
    /**
156
     * Logger instance.
157
     *
158
     * @var ActionLogger
159
     */
160
    public $logger;
161
162
    /**
163
     * The closure functions that will be executed in this->run()
164
     *
165
     * @var array
166
     */
167
    protected $closures = [];
168
169
    /**
170
     * default settings
171
     *
172
     * @var array
173
     */
174
    protected $properties = [];
175
176
    /**
177
     * This is for creating data tables automatically
178
     * Turn it off, if you don't want to check data tables every connection.
179
     *
180
     * @var bool
181
     */
182
    protected $autoCreateDatabase = true;
183
184
    /**
185
     * Container for captcha addons.
186
     * The collection of \Shieldon\Firewall\Captcha\CaptchaInterface
187
     *
188
     * @var array
189
     */
190
    protected $captcha = [];
191
192
    /**
193
     * The ways Shieldon send a message to when someone has been blocked.
194
     * The collection of \Shieldon\Messenger\Messenger\MessengerInterface
195
     *
196
     * @var array
197
     */
198
    protected $messenger = [];
199
200
    /**
201
     * Result.
202
     *
203
     * @var int
204
     */
205
    protected $result = 1;
206
207
    /**
208
     * URLs that are excluded from Shieldon's protection.
209
     *
210
     * @var array
211
     */
212
    protected $excludedUrls = [];
213
214
    /**
215
     * Which type of configuration source that Shieldon firewall managed?
216
     *
217
     * @var string
218
     */
219
    protected $firewallType = 'self'; // managed | config | self | demo
220
221
    /**
222
     * Custom dialog UI settings.
223
     *
224
     * @var array
225
     */
226
    protected $dialogUI = [];
227
228
    /**
229
     * Strict mode.
230
     * 
231
     * Set by `strictMode()` only. The default value of this propertry is undefined.
232
     *
233
     * @var bool|null
234
     */
235
    protected $strictMode;
236
237
    /**
238
     * The directory in where the frontend template files are placed.
239
     *
240
     * @var string
241
     */
242
    protected $templateDirectory = '';
243
244
    /**
245
     * The message that will be sent to the third-party API.
246
     *
247
     * @var string
248
     */
249
    protected $msgBody = '';
250
251
    /**
252
     * Shieldon constructor.
253
     * 
254
     * @param ServerRequestInterface|null $request  A PSR-7 server request.
255
     * 
256
     * @return void
257
     */
258
    public function __construct(?ServerRequestInterface $request  = null, ?ResponseInterface $response = null)
259
    {
260
        // Load helper functions. This is the must.
261
        new Helpers();
262
263
        $request = $request ?? HttpFactory::createRequest();
264
        $response = $response ?? HttpFactory::createResponse();
265
        $session = HttpFactory::createSession();
266
267
        $this->properties = get_default_properties();
268
        $this->setCaptcha(new Foundation());
269
270
        Container::set('request', $request);
271
        Container::set('response', $response);
272
        Container::set('session', $session);
273
        Container::set('shieldon', $this);
274
    }
275
276
    /**
277
     * Log actions.
278
     *
279
     * @param int $actionCode The code number of the action.
280
     *
281
     * @return void
282
     */
283
    protected function log(int $actionCode): void
284
    {
285
        if (!$this->logger) {
286
            return;
287
        }
288
289
        $logData = [];
290
        $logData['ip'] = $this->getIp();
291
        $logData['session_id'] = get_session()->get('id');
292
        $logData['action_code'] = $actionCode;
293
        $logData['timesamp'] = time();
294
295
        $this->logger->add($logData);
296
    }
297
298
    /**
299
     * Initialize components.
300
     *
301
     * @return void
302
     */
303
    private function initComponents()
304
    {
305
        foreach (array_keys($this->component) as $name) {
306
            $this->component[$name]->setIp($this->ip);
307
            $this->component[$name]->setRdns($this->rdns);
308
309
            // Apply global strict mode to all components by `strictMode()` if nesscessary.
310
            if (isset($this->strictMode)) {
311
                $this->component[$name]->setStrict($this->strictMode);
312
            }
313
        }
314
    }
315
316
    /**
317
     * Check if current IP is trusted or not.
318
     *
319
     * @return bool
320
     */
321
    private function isTrustedBot()
322
    {
323
        if ($this->getComponent('TrustedBot')) {
324
325
            // We want to put all the allowed robot into the rule list, so that the checking of IP's resolved hostname 
326
            // is no more needed for that IP.
327
            if ($this->getComponent('TrustedBot')->isAllowed()) {
0 ignored issues
show
Bug introduced by
The method isAllowed() does not exist on Shieldon\Firewall\Component\ComponentInterface. It seems like you code against a sub-type of Shieldon\Firewall\Component\ComponentInterface such as Shieldon\Firewall\Component\Ip or Shieldon\Firewall\Component\TrustedBot. ( Ignorable by Annotation )

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

327
            if ($this->getComponent('TrustedBot')->/** @scrutinizer ignore-call */ isAllowed()) {
Loading history...
328
329
                if ($this->getComponent('TrustedBot')->isGoogle()) {
0 ignored issues
show
Bug introduced by
The method isGoogle() does not exist on Shieldon\Firewall\Component\ComponentInterface. It seems like you code against a sub-type of Shieldon\Firewall\Component\ComponentInterface such as Shieldon\Firewall\Component\TrustedBot. ( Ignorable by Annotation )

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

329
                if ($this->getComponent('TrustedBot')->/** @scrutinizer ignore-call */ isGoogle()) {
Loading history...
330
                    // Add current IP into allowed list, because it is from real Google domain.
331
                    $this->action(
332
                        self::ACTION_ALLOW,
333
                        self::REASON_IS_GOOGLE
334
                    );
335
336
                } elseif ($this->getComponent('TrustedBot')->isBing()) {
0 ignored issues
show
Bug introduced by
The method isBing() does not exist on Shieldon\Firewall\Component\ComponentInterface. It seems like you code against a sub-type of Shieldon\Firewall\Component\ComponentInterface such as Shieldon\Firewall\Component\TrustedBot. ( Ignorable by Annotation )

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

336
                } elseif ($this->getComponent('TrustedBot')->/** @scrutinizer ignore-call */ isBing()) {
Loading history...
337
                    // Add current IP into allowed list, because it is from real Bing domain.
338
                    $this->action(
339
                        self::ACTION_ALLOW,
340
                        self::REASON_IS_BING
341
                    );
342
343
                } elseif ($this->getComponent('TrustedBot')->isYahoo()) {
0 ignored issues
show
Bug introduced by
The method isYahoo() does not exist on Shieldon\Firewall\Component\ComponentInterface. It seems like you code against a sub-type of Shieldon\Firewall\Component\ComponentInterface such as Shieldon\Firewall\Component\TrustedBot. ( Ignorable by Annotation )

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

343
                } elseif ($this->getComponent('TrustedBot')->/** @scrutinizer ignore-call */ isYahoo()) {
Loading history...
344
                    // Add current IP into allowed list, because it is from real Yahoo domain.
345
                    $this->action(
346
                        self::ACTION_ALLOW,
347
                        self::REASON_IS_YAHOO
348
                    );
349
350
                } else {
351
                    // Add current IP into allowed list, because you trust it.
352
                    // You have already defined it in the settings.
353
                    $this->action(
354
                        self::ACTION_ALLOW,
355
                        self::REASON_IS_SEARCH_ENGINE
356
                    );
357
                }
358
                // Allowed robots not join to our traffic handler.
359
                $this->result = self::RESPONSE_ALLOW;
360
                return true;
361
            }
362
        }
363
        return false;
364
    }
365
366
    /**
367
     * Check whether the IP is fake search engine or not.
368
     * The method "isTrustedBot()" must be executed before this method.
369
     *
370
     * @return bool
371
     */
372
    private function isFakeRobot(): bool
373
    {
374
        if ($this->getComponent('TrustedBot')) {
375
            if ($this->getComponent('TrustedBot')->isFakeRobot()) {
0 ignored issues
show
Bug introduced by
The method isFakeRobot() does not exist on Shieldon\Firewall\Component\ComponentInterface. It seems like you code against a sub-type of Shieldon\Firewall\Component\ComponentInterface such as Shieldon\Firewall\Component\TrustedBot. ( Ignorable by Annotation )

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

375
            if ($this->getComponent('TrustedBot')->/** @scrutinizer ignore-call */ isFakeRobot()) {
Loading history...
376
                $this->action(
377
                    self::ACTION_DENY,
378
                    self::REASON_COMPONENT_TRUSTED_ROBOT
379
                );
380
                $this->result = self::RESPONSE_DENY;
381
                return true;
382
            }
383
        }
384
        return false;
385
    }
386
387
    /**
388
     * Run, run, run!
389
     *
390
     * Check the rule tables first, if an IP address has been listed.
391
     * Call function filter() if an IP address is not listed in rule tables.
392
     *
393
     * @return int The response code.
394
     */
395
    protected function process(): int
396
    {
397
        $this->driver->init($this->autoCreateDatabase);
398
399
        $this->initComponents();
400
401
        /*
402
        |--------------------------------------------------------------------------
403
        | Stage - Looking for rule table.
404
        |--------------------------------------------------------------------------
405
        */
406
407
        if ($this->DoesRuleExist()) {
408
            return $this->result;
409
        }
410
411
        /*
412
        |--------------------------------------------------------------------------
413
        | Statge - Detect popular search engine.
414
        |--------------------------------------------------------------------------
415
        */
416
417
        if ($this->isTrustedBot()) {
418
            return $this->result;
419
        }
420
421
        if ($this->isFakeRobot()) {
422
            return $this->result;
423
        }
424
        
425
        /*
426
        |--------------------------------------------------------------------------
427
        | Stage - IP component.
428
        |--------------------------------------------------------------------------
429
        */
430
431
        if ($this->getComponent('Ip')) {
432
433
            $result = $this->getComponent('Ip')->check();
0 ignored issues
show
Bug introduced by
The method check() does not exist on Shieldon\Firewall\Component\ComponentInterface. It seems like you code against a sub-type of Shieldon\Firewall\Component\ComponentInterface such as Shieldon\Firewall\Component\Ip. ( Ignorable by Annotation )

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

433
            $result = $this->getComponent('Ip')->/** @scrutinizer ignore-call */ check();
Loading history...
434
            $actionCode = self::ACTION_DENY;
435
436
            if (!empty($result)) {
437
438
                switch ($result['status']) {
439
440
                    case 'allow':
441
                        $actionCode = self::ACTION_ALLOW;
442
                        $reasonCode = $result['code'];
443
                        break;
444
    
445
                    case 'deny':
446
                        $actionCode = self::ACTION_DENY;
447
                        $reasonCode = $result['code']; 
448
                        break;
449
                }
450
451
                $this->action($actionCode, $reasonCode);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $reasonCode does not seem to be defined for all execution paths leading up to this point.
Loading history...
452
453
                // $resultCode = $actionCode
454
                return $this->result = $this->sessionHandler($actionCode);
455
            }
456
        }
457
458
        /*
459
        |--------------------------------------------------------------------------
460
        | Stage - Check all other components.
461
        |--------------------------------------------------------------------------
462
        */
463
464
        foreach ($this->component as $component) {
465
466
            // check if is a a bad robot already defined in settings.
467
            if ($component->isDenied()) {
468
469
                // @since 0.1.8
470
                $this->action(
471
                    self::ACTION_DENY,
472
                    $component->getDenyStatusCode()
473
                );
474
475
                return $this->result = self::RESPONSE_DENY;
476
            }
477
        }
478
479
        /*
480
        |--------------------------------------------------------------------------
481
        | Stage - Filters
482
        |--------------------------------------------------------------------------
483
        | This IP address is not listed in rule table, let's detect it.
484
        |
485
        */
486
487
        if (array_search(true, $this->filterStatus)) {
488
            return $this->result = $this->sessionHandler($this->filter());
489
        }
490
491
        return $this->result = $this->sessionHandler(self::RESPONSE_ALLOW);
492
    }
493
494
    /**
495
     * Start an action for this IP address, allow or deny, and give a reason for it.
496
     *
497
     * @param int    $actionCode - 0: deny, 1: allow, 9: unban.
498
     * @param string $reasonCode
499
     * @param string $assignIp
500
     * 
501
     * @return void
502
     */
503
    protected function action(int $actionCode, int $reasonCode, string $assignIp = ''): void
504
    {
505
        $ip = $this->ip;
506
        $rdns = $this->rdns;
507
        $now = time();
508
        $logData = [];
509
    
510
        if ('' !== $assignIp) {
511
            $ip = $assignIp;
512
            $rdns = gethostbyaddr($ip);
513
        }
514
515
        switch ($actionCode) {
516
            case self::ACTION_ALLOW: // acutally not used.
517
            case self::ACTION_DENY:  // actually not used.
518
            case self::ACTION_TEMPORARILY_DENY:
519
                $logData['log_ip']     = $ip;
520
                $logData['ip_resolve'] = $rdns;
521
                $logData['time']       = $now;
522
                $logData['type']       = $actionCode;
523
                $logData['reason']     = $reasonCode;
524
                $logData['attempts']   = 0;
525
526
                $this->driver->save($ip, $logData, 'rule');
527
                break;
528
            
529
            case self::ACTION_UNBAN:
530
                $this->driver->delete($ip, 'rule');
531
                break;
532
        }
533
534
        // Remove logs for this IP address because It already has it's own rule on system.
535
        // No need to count it anymore.
536
        $this->driver->delete($ip, 'filter');
537
538
        if (null !== $this->logger) {
539
            $log['ip']          = $ip;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$log was never initialized. Although not strictly required by PHP, it is generally a good practice to add $log = array(); before regardless.
Loading history...
540
            $log['session_id']  = get_session()->get('id');
541
            $log['action_code'] = $actionCode;
542
            $log['timesamp']    = $now;
543
544
            $this->logger->add($log);
545
        }
546
    }
547
548
    // @codeCoverageIgnoreEnd
549
550
    /*
551
    | -------------------------------------------------------------------
552
    |                            Public APIs
553
    | -------------------------------------------------------------------
554
    */
555
556
    /**
557
     * Set a commponent.
558
     *
559
     * @param ComponentProvider $instance
560
     *
561
     * @return void
562
     */
563
    public function setComponent(ComponentProvider $instance): void
564
    {
565
        $class = $this->getClassName($instance);
566
        $this->component[$class] = $instance;
567
    }
568
569
    /**
570
     * Set a captcha.
571
     *
572
     * @param CaptchaInterface $instance
573
     *
574
     * @return void
575
     */
576
    public function setCaptcha(CaptchaInterface $instance): void
577
    {
578
        $class = $this->getClassName($instance);
579
        $this->captcha[$class] = $instance;
580
    }
581
582
    /**
583
     * Set a data driver.
584
     *
585
     * @param DriverProvider $driver Query data from the driver you choose to use.
586
     *
587
     * @return void
588
     */
589
    public function setDriver(DriverProvider $driver): void
590
    {
591
        $this->driver = $driver;
592
    }
593
594
    /**
595
     * Set a action log logger.
596
     *
597
     * @param ActionLogger $logger
598
     *
599
     * @return void
600
     */
601
    public function setLogger(ActionLogger $logger): void
602
    {
603
        $this->logger = $logger;
604
    }
605
606
    /**
607
     * Set a messenger
608
     *
609
     * @param MessengerInterfa $instance
0 ignored issues
show
Bug introduced by
The type Shieldon\Firewall\MessengerInterfa 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...
610
     *
611
     * @return void
612
     */
613
    public function setMessenger(MessengerInterface $instance): void
614
    {
615
        $class = $this->getClassName($instance);
616
        $this->messengers[$class] = $instance;
0 ignored issues
show
Bug Best Practice introduced by
The property messengers does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
617
    }
618
619
    /**
620
     * Get a component instance from component's container.
621
     *
622
     * @param string $name The component's class name.
623
     *
624
     * @return ComponentInterface|null
625
     */
626
    public function getComponent(string $name)
627
    {
628
        if (isset($this->component[$name])) {
629
            return $this->component[$name];
630
        }
631
632
        return null;
633
    }
634
635
    /**
636
     * Strict mode.
637
     * This option will take effects to all components.
638
     * 
639
     * @param bool $bool Set true to enble strict mode, false to disable it overwise.
640
     *
641
     * @return void
642
     */
643
    public function setStrict(bool $bool)
644
    {
645
        $this->strictMode = $bool;
646
    }
647
648
    /**
649
     * Disable filters.
650
     */
651
    public function disableFilters(): void
652
    {
653
        $this->setFilters([
654
            'session'   => false,
655
            'cookie'    => false,
656
            'referer'   => false,
657
            'frequency' => false,
658
        ]);
659
    }
660
661
    /**
662
     * For first time installation only. This is for creating data tables automatically.
663
     * Turning it on will check the data tables exist or not at every single pageview, 
664
     * it's not good for high traffic websites.
665
     *
666
     * @param bool $bool
667
     * 
668
     * @return void
669
     */
670
    public function createDatabase(bool $bool)
671
    {
672
        $this->autoCreateDatabase = $bool;
673
    }
674
675
    /**
676
     * Set a data channel.
677
     *
678
     * This will create databases for the channel.
679
     *
680
     * @param string $channel Specify a channel.
681
     *
682
     * @return void
683
     */
684
    public function setChannel(string $channel)
685
    {
686
        if (!$this->driver instanceof DriverProvider) {
0 ignored issues
show
introduced by
$this->driver is always a sub-type of Shieldon\Firewall\Driver\DriverProvider.
Loading history...
687
            throw new LogicException('setChannel method requires setDriver set first.');
688
        } else {
689
            $this->driver->setChannel($channel);
690
        }
691
    }
692
693
    /**
694
     * Return the result from Captchas.
695
     *
696
     * @return bool
697
     */
698
    public function captchaResponse(): bool
699
    {
700
        foreach ($this->captcha as $captcha) {
701
            
702
            if (!$captcha->response()) {
703
                return false;
704
            }
705
        }
706
707
        if (!empty($this->sessionLimit['count'])) {
708
            $this->result = $this->sessionHandler(self::RESPONSE_ALLOW);
709
        }
710
711
        return true;
712
    }
713
714
    /**
715
     * Ban an IP.
716
     *
717
     * @param string $ip A valid IP address.
718
     *
719
     * @return void
720
     */
721
    public function ban(string $ip = ''): void
722
    {
723
        if ('' === $ip) {
724
            $ip = $this->ip;
725
        }
726
 
727
        $this->action(
728
            self::ACTION_DENY,
729
            self::REASON_MANUAL_BAN, $ip
730
        );
731
    }
732
733
    /**
734
     * Unban an IP.
735
     *
736
     * @param string $ip A valid IP address.
737
     *
738
     * @return void
739
     */
740
    public function unban(string $ip = ''): void
741
    {
742
        if ('' === $ip) {
743
            $ip = $this->ip;
744
        }
745
746
        $this->action(
747
            self::ACTION_UNBAN,
748
            self::REASON_MANUAL_BAN, $ip
749
        );
750
        $this->log(self::ACTION_UNBAN);
751
752
        $this->result = self::RESPONSE_ALLOW;
753
    }
754
755
    /**
756
     * Set a property setting.
757
     *
758
     * @param string $key   The key of a property setting.
759
     * @param mixed  $value The value of a property setting.
760
     *
761
     * @return void
762
     */
763
    public function setProperty(string $key = '', $value = '')
764
    {
765
        if (isset($this->properties[$key])) {
766
            $this->properties[$key] = $value;
767
        }
768
    }
769
770
    /**
771
     * Set the property settings.
772
     * 
773
     * @param array $settings The settings.
774
     *
775
     * @return void
776
     */
777
    public function setProperties(array $settings): void
778
    {
779
        foreach (array_keys($this->properties) as $k) {
780
            if (isset($settings[$k])) {
781
                $this->properties[$k] = $settings[$k];
782
            }
783
        }
784
    }
785
786
    /**
787
     * Limt online sessions.
788
     *
789
     * @param int $count
790
     * @param int $period
791
     *
792
     * @return void
793
     */
794
    public function limitSession(int $count = 1000, int $period = 300): void
795
    {
796
        $this->sessionLimit = [
797
            'count' => $count,
798
            'period' => $period
799
        ];
800
    }
801
802
    /**
803
     * Customize the dialog UI.
804
     *
805
     * @return void
806
     */
807
    public function setDialogUI(array $settings): void
808
    {
809
        $this->dialogUI = $settings;
810
    }
811
812
    /**
813
     * Set the frontend template directory.
814
     *
815
     * @param string $directory
816
     *
817
     * @return void
818
     */
819
    public function setTemplateDirectory(string $directory)
820
    {
821
        if (!is_dir($directory)) {
822
            throw new InvalidArgumentException('The template directory does not exist.');
823
        }
824
        $this->templateDirectory = $directory;
825
    }
826
827
    /**
828
     * Get a template PHP file.
829
     *
830
     * @param string $type The template type.
831
     *
832
     * @return string
833
     */
834
    protected function getTemplate(string $type): string
835
    {
836
        $directory = self::KERNEL_DIR . '/../../templates/frontend';
837
838
        if (!empty($this->templateDirectory)) {
839
            $directory = $this->templateDirectory;
840
        }
841
842
        $path = $directory . '/' . $type . '.php';
843
844
        if (!file_exists($path)) {
845
            throw new RuntimeException(
846
                sprintf(
847
                    'The templeate file is missing. (%s)',
848
                    $path
849
                )
850
            );
851
        }
852
853
        return $path;
854
    }
855
856
    /**
857
     * Get a class name without namespace string.
858
     *
859
     * @param object $instance Class
860
     * 
861
     * @return void
862
     */
863
    protected function getClassName($instance): string
864
    {
865
        $class = get_class($instance);
866
        return substr($class, strrpos($class, '\\') + 1); 
0 ignored issues
show
Bug Best Practice introduced by
The expression return substr($class, strrpos($class, '\') + 1) returns the type string which is incompatible with the documented return type void.
Loading history...
867
    }
868
869
    /**
870
     * Respond the result.
871
     *
872
     * @return ResponseInterface
873
     */
874
    public function respond(): ResponseInterface
875
    {
876
        $response = get_response();
877
        $type = '';
878
879
        if (self::RESPONSE_TEMPORARILY_DENY === $this->result) {
880
            $type = 'captcha';
881
            $statusCode = 403; // Forbidden.
882
883
        } elseif (self::RESPONSE_LIMIT_SESSION === $this->result) {
884
            $type = 'session_limitation';
885
            $statusCode = 429; // Too Many Requests.
886
887
        } elseif (self::RESPONSE_DENY === $this->result) {
888
            $type = 'rejection';
889
            $statusCode = 400; // Bad request.
890
        }
891
892
        // Nothing happened. Return.
893
        if (empty($type)) {
894
            // @codeCoverageIgnoreStart
895
            return $response;
896
            // @codeCoverageIgnoreEnd
897
        }
898
899
        $viewPath = $this->getTemplate($type);
900
901
        // The language of output UI. It is used on views.
902
        $langCode = get_session()->get('shieldon_ui_lang') ?? 'en';
903
        // Show online session count. It is used on views.
904
        $showOnlineInformation = true;
905
        // Show user information such as IP, user-agent, device name.
906
        $showUserInformation = true;
907
908
        if (empty($this->properties['display_online_info'])) {
909
            $showOnlineInformation = false;
910
        }
911
912
        if (empty($this->properties['display_user_info'])) {
913
            $showUserInformation = false;
914
        }
915
916
        if ($showUserInformation) {
917
            $dialoguserinfo['ip'] = $this->ip;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$dialoguserinfo was never initialized. Although not strictly required by PHP, it is generally a good practice to add $dialoguserinfo = array(); before regardless.
Loading history...
918
            $dialoguserinfo['rdns'] = $this->rdns;
919
            $dialoguserinfo['user_agent'] = get_request()->getHeaderLine('user-agent');
920
        }
921
922
        $ui = [
923
            'background_image' => $this->dialogUI['background_image'] ?? '',
924
            'bg_color'         => $this->dialogUI['bg_color']         ?? '#ffffff',
925
            'header_bg_color'  => $this->dialogUI['header_bg_color']  ?? '#212531',
926
            'header_color'     => $this->dialogUI['header_color']     ?? '#ffffff',
927
            'shadow_opacity'   => $this->dialogUI['shadow_opacity']   ?? '0.2',
928
        ];
929
930
        if (!defined('SHIELDON_VIEW')) {
931
            define('SHIELDON_VIEW', true);
932
        }
933
934
        $css = require $this->getTemplate('css/default');
935
936
        ob_start();
937
        require $viewPath;
938
        $output = ob_get_contents();
939
        ob_end_clean();
940
941
        // Remove unused variable notices generated from PHP intelephense.
942
        unset(
943
            $css,
944
            $ui,
945
            $langCode,
946
            $showOnlineInformation,
947
            $showLineupInformation,
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $showLineupInformation seems to be never defined.
Loading history...
948
            $showUserInformation
949
        );
950
951
        $stream = $response->getBody();
952
        $stream->write($output);
953
        $stream->rewind();
954
955
        return $response->
956
            withHeader('X-Protected-By', 'shieldon.io')->
957
            withBody($stream)->
958
            withStatus($statusCode);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $statusCode does not seem to be defined for all execution paths leading up to this point.
Loading history...
959
    }
960
961
    /**
962
     * Run, run, run!
963
     *
964
     * Check the rule tables first, if an IP address has been listed.
965
     * Call function filter() if an IP address is not listed in rule tables.
966
     *
967
     * @return 
968
     */
969
    public function run(): int
970
    {
971
        if (!isset($this->driver)) {
972
            throw new RuntimeException(
973
                'Must register at least one data driver.'
974
            );
975
        }
976
        
977
        // Ignore the excluded urls.
978
        if (!empty($this->excludedUrls)) {
979
            foreach ($this->excludedUrls as $url) {
980
                if (0 === strpos(get_request()->getUri()->getPath(), $url)) {
981
                    return $this->result = self::RESPONSE_ALLOW;
982
                }
983
            }
984
        }
985
986
        // Execute closure functions.
987
        foreach ($this->closures as $closure) {
988
            $closure();
989
        }
990
991
        $result = $this->process();
992
993
        if ($result !== self::RESPONSE_ALLOW) {
994
995
            // Current session did not pass the CAPTCHA, it is still stuck in CAPTCHA page.
996
            $actionCode = self::LOG_CAPTCHA;
997
998
            // If current session's respone code is RESPONSE_DENY, record it as `blacklist_count` in our logs.
999
            // It is stuck in warning page, not CAPTCHA.
1000
            if ($result === self::RESPONSE_DENY) {
1001
                $actionCode = self::LOG_BLACKLIST;
1002
            }
1003
1004
            if ($result === self::RESPONSE_LIMIT_SESSION) {
1005
                $actionCode = self::LOG_LIMIT;
1006
            }
1007
1008
            $this->log($actionCode);
1009
1010
        } else {
1011
1012
            $this->log(self::LOG_PAGEVIEW);
1013
        }
1014
1015
 
1016
        if (!empty($this->msgBody)) {
1017
 
1018
            // @codeCoverageIgnoreStart
1019
1020
            try {
1021
                foreach ($this->messenger as $messenger) {
1022
                    $messenger->setTimeout(2);
1023
                    $messenger->send($this->msgBody);
1024
                }
1025
            } catch (RuntimeException $e) {
1026
                // Do not throw error, becasue the third-party services might be unavailable.
1027
            }
1028
1029
            // @codeCoverageIgnoreEnd
1030
        }
1031
1032
1033
        return $result;
1034
    }
1035
1036
    /**
1037
     * Set the filters.
1038
     *
1039
     * @param array $settings filter settings.
1040
     *
1041
     * @return void
1042
     */
1043
    public function setFilters(array $settings)
1044
    {
1045
        foreach (array_keys($this->filterStatus) as $k) {
1046
            if (isset($settings[$k])) {
1047
                $this->filterStatus[$k] = $settings[$k] ?? false;
1048
            }
1049
        }
1050
    }
1051
1052
    /**
1053
     * Set a filter.
1054
     *
1055
     * @param string $filterName The filter's name.
1056
     * @param bool   $value      True for enabling the filter, overwise.
1057
     *
1058
     * @return void
1059
     */
1060
    public function setFilter(string $filterName, bool $value): void
1061
    {
1062
        if (isset($this->filterStatus[$filterName])) {
1063
            $this->filterStatus[$filterName] = $value;
1064
        }
1065
    }
1066
1067
1068
1069
    /**
1070
     * Set the URLs you want them to be excluded them from protection.
1071
     *
1072
     * @param array $urls The list of URL want to be excluded.
1073
     *
1074
     * @return void
1075
     */
1076
    public function setExcludedUrls(array $urls = []): void
1077
    {
1078
        $this->excludedUrls = $urls;
1079
    }
1080
1081
    /**
1082
     * Set a closure function.
1083
     *
1084
     * @param string  $key     The name for the closure class.
1085
     * @param Closure $closure An instance will be later called.
1086
     *
1087
     * @return void
1088
     */
1089
    public function setClosure(string $key, Closure $closure): void
1090
    {
1091
        $this->closures[$key] = $closure;
1092
    }
1093
1094
    /**
1095
     * Print a JavasSript snippet in your webpages.
1096
     * 
1097
     * This snippet generate cookie on client's browser,then we check the 
1098
     * cookie to identify the client is a rebot or not.
1099
     *
1100
     * @return string
1101
     */
1102
    public function outputJsSnippet(): string
1103
    {
1104
        $tmpCookieName = $this->properties['cookie_name'];
1105
        $tmpCookieDomain = $this->properties['cookie_domain'];
1106
1107
        if (empty($tmpCookieDomain) && get_request()->getHeaderLine('host')) {
1108
            $tmpCookieDomain = get_request()->getHeaderLine('host');
1109
        }
1110
1111
        $tmpCookieValue = $this->properties['cookie_value'];
1112
1113
        $jsString = '
1114
            <script>
1115
                var d = new Date();
1116
                d.setTime(d.getTime()+(60*60*24*30));
1117
                document.cookie = "' . $tmpCookieName . '=' . $tmpCookieValue . ';domain=.' . $tmpCookieDomain . ';expires="+d.toUTCString();
1118
            </script>
1119
        ';
1120
1121
        return $jsString;
1122
    }
1123
1124
    /**
1125
     * Get current visior's path.
1126
     *
1127
     * @return string
1128
     */
1129
    public function getCurrentUrl(): string
1130
    {
1131
        return get_request()->getUri()->getPath();
1132
    }
1133
1134
    /**
1135
     * Displayed on Firewall Panel, tell you current what type of current
1136
     * configuration is used for.
1137
     * 
1138
     * @param string $type The type of configuration.
1139
     *                     demo | managed | config
1140
     *
1141
     * @return void
1142
     */
1143
    public function managedBy(string $type = ''): void
1144
    {
1145
        if (in_array($type, ['managed', 'config', 'demo'])) {
1146
            $this->firewallType = $type;
1147
        }
1148
    }
1149
}
1150