Passed
Push — 2.x ( 192121...9a09f2 )
by Terry
03:49
created

Kernel::setChannel()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
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\ComponentTrait;
48
use Shieldon\Firewall\Kernel\RuleTrait;
49
use Shieldon\Firewall\Kernel\LimitSessionTrait;
50
use Shieldon\Messenger\Messenger\MessengerInterface;
51
use function Shieldon\Firewall\__;
52
use function Shieldon\Firewall\get_cpu_usage;
53
use function Shieldon\Firewall\get_default_properties;
54
use function Shieldon\Firewall\get_memory_usage;
55
use function Shieldon\Firewall\get_request;
56
use function Shieldon\Firewall\get_response;
57
use function Shieldon\Firewall\get_session;
58
59
60
use Closure;
61
use InvalidArgumentException;
62
use LogicException;
63
use RuntimeException;
64
use function file_exists;
65
use function file_put_contents;
66
use function filter_var;
67
use function get_class;
68
use function gethostbyaddr;
69
use function is_dir;
70
use function is_writable;
71
use function microtime;
72
use function ob_end_clean;
73
use function ob_get_contents;
74
use function ob_start;
75
use function str_replace;
76
use function strpos;
77
use function strrpos;
78
use function substr;
79
use function time;
80
81
/**
82
 * The primary Shiendon class.
83
 */
84
class Kernel
85
{
86
    use IpTrait;
87
    use FilterTrait;
88
    use ComponentTrait;
89
    use RuleTrait;
90
    use LimitSessionTrait;
91
92
    // Reason codes (allow)
93
    const REASON_IS_SEARCH_ENGINE = 100;
94
    const REASON_IS_GOOGLE = 101;
95
    const REASON_IS_BING = 102;
96
    const REASON_IS_YAHOO = 103;
97
    const REASON_IS_SOCIAL_NETWORK = 110;
98
    const REASON_IS_FACEBOOK = 111;
99
    const REASON_IS_TWITTER = 112;
100
101
    // Reason codes (deny)
102
    const REASON_TOO_MANY_SESSIONS = 1;
103
    const REASON_TOO_MANY_ACCESSES = 2; // (not used)
104
    const REASON_EMPTY_JS_COOKIE = 3;
105
    const REASON_EMPTY_REFERER = 4;
106
    
107
    const REASON_REACHED_LIMIT_DAY = 11;
108
    const REASON_REACHED_LIMIT_HOUR = 12;
109
    const REASON_REACHED_LIMIT_MINUTE = 13;
110
    const REASON_REACHED_LIMIT_SECOND = 14;
111
112
    const REASON_INVALID_IP = 40;
113
    const REASON_DENY_IP = 41;
114
    const REASON_ALLOW_IP = 42;
115
116
    const REASON_COMPONENT_IP = 81;
117
    const REASON_COMPONENT_RDNS = 82;
118
    const REASON_COMPONENT_HEADER = 83;
119
    const REASON_COMPONENT_USERAGENT = 84;
120
    const REASON_COMPONENT_TRUSTED_ROBOT = 85;
121
122
    const REASON_MANUAL_BAN = 99;
123
124
    // Action codes
125
    const ACTION_DENY = 0;
126
    const ACTION_ALLOW = 1;
127
    const ACTION_TEMPORARILY_DENY = 2;
128
    const ACTION_UNBAN = 9;
129
130
    // Result codes
131
    const RESPONSE_DENY = 0;
132
    const RESPONSE_ALLOW = 1;
133
    const RESPONSE_TEMPORARILY_DENY = 2;
134
    const RESPONSE_LIMIT_SESSION = 3;
135
136
    const LOG_LIMIT = 3;
137
    const LOG_PAGEVIEW = 11;
138
    const LOG_BLACKLIST = 98;
139
    const LOG_CAPTCHA = 99;
140
141
    const KERNEL_DIR = __DIR__;
142
143
    /**
144
     * Driver for storing data.
145
     *
146
     * @var \Shieldon\Firewall\Driver\DriverProvider
147
     */
148
    public $driver;
149
150
    /**
151
     * Logger instance.
152
     *
153
     * @var ActionLogger
154
     */
155
    public $logger;
156
157
    /**
158
     * The closure functions that will be executed in this->run()
159
     *
160
     * @var array
161
     */
162
    protected $closures = [];
163
164
    /**
165
     * default settings
166
     *
167
     * @var array
168
     */
169
    protected $properties = [];
170
171
    /**
172
     * This is for creating data tables automatically
173
     * Turn it off, if you don't want to check data tables every connection.
174
     *
175
     * @var bool
176
     */
177
    protected $autoCreateDatabase = true;
178
179
    /**
180
     * Container for captcha addons.
181
     * The collection of \Shieldon\Firewall\Captcha\CaptchaInterface
182
     *
183
     * @var array
184
     */
185
    public $captcha = [];
186
187
    /**
188
     * The ways Shieldon send a message to when someone has been blocked.
189
     * The collection of \Shieldon\Messenger\Messenger\MessengerInterface
190
     *
191
     * @var array
192
     */
193
    protected $messenger = [];
194
195
    /**
196
     * Result.
197
     *
198
     * @var int
199
     */
200
    protected $result = 1;
201
202
    /**
203
     * URLs that are excluded from Shieldon's protection.
204
     *
205
     * @var array
206
     */
207
    protected $excludedUrls = [];
208
209
    /**
210
     * Which type of configuration source that Shieldon firewall managed?
211
     *
212
     * @var string
213
     */
214
    protected $firewallType = 'self'; // managed | config | self | demo
215
216
    /**
217
     * Custom dialog UI settings.
218
     *
219
     * @var array
220
     */
221
    protected $dialogUI = [];
222
223
    /**
224
     * Strict mode.
225
     * 
226
     * Set by `strictMode()` only. The default value of this propertry is undefined.
227
     *
228
     * @var bool|null
229
     */
230
    protected $strictMode;
231
232
    /**
233
     * The directory in where the frontend template files are placed.
234
     *
235
     * @var string
236
     */
237
    protected $templateDirectory = '';
238
239
    /**
240
     * The message that will be sent to the third-party API.
241
     *
242
     * @var string
243
     */
244
    protected $msgBody = '';
245
246
    /**
247
     * Shieldon constructor.
248
     * 
249
     * @param ServerRequestInterface|null $request  A PSR-7 server request.
250
     * 
251
     * @return void
252
     */
253
    public function __construct(?ServerRequestInterface $request  = null, ?ResponseInterface $response = null)
254
    {
255
        // Load helper functions. This is the must.
256
        new Helpers();
257
258
        $request = $request ?? HttpFactory::createRequest();
259
        $response = $response ?? HttpFactory::createResponse();
260
        $session = HttpFactory::createSession();
261
262
        $this->properties = get_default_properties();
263
        $this->setCaptcha(new Foundation());
264
265
        Container::set('request', $request);
266
        Container::set('response', $response);
267
        Container::set('session', $session);
268
        Container::set('shieldon', $this);
269
    }
270
271
    /**
272
     * Log actions.
273
     *
274
     * @param int $actionCode The code number of the action.
275
     *
276
     * @return void
277
     */
278
    protected function log(int $actionCode, $ip = ''): void
279
    {
280
        if (!$this->logger) {
281
            return;
282
        }
283
284
        $logData = [];
285
        $logData['ip'] = $ip ?? $this->getIp();
286
        $logData['session_id'] = get_session()->get('id');
287
        $logData['action_code'] = $actionCode;
288
        $logData['timesamp'] = time();
289
290
        $this->logger->add($logData);
291
    }
292
293
    /**
294
     * Run, run, run!
295
     *
296
     * Check the rule tables first, if an IP address has been listed.
297
     * Call function filter() if an IP address is not listed in rule tables.
298
     *
299
     * @return int The response code.
300
     */
301
    protected function process(): int
302
    {
303
        $this->driver->init($this->autoCreateDatabase);
304
305
        $this->initComponents();
306
307
        /*
308
        |--------------------------------------------------------------------------
309
        | Stage - Looking for rule table.
310
        |--------------------------------------------------------------------------
311
        */
312
313
        if ($this->IsRuleExist()) {
314
            return $this->result;
315
        }
316
317
        /*
318
        |--------------------------------------------------------------------------
319
        | Statge - Detect popular search engine.
320
        |--------------------------------------------------------------------------
321
        */
322
323
        if ($this->isTrustedBot()) {
324
            return $this->result;
325
        }
326
327
        if ($this->isFakeRobot()) {
328
            return $this->result;
329
        }
330
        
331
        /*
332
        |--------------------------------------------------------------------------
333
        | Stage - IP component.
334
        |--------------------------------------------------------------------------
335
        */
336
337
        if ($this->isIpComponent()) {
338
            return $this->result;
339
        }
340
341
        /*
342
        |--------------------------------------------------------------------------
343
        | Stage - Check all other components.
344
        |--------------------------------------------------------------------------
345
        */
346
347
        foreach ($this->component as $component) {
348
349
            // check if is a a bad robot already defined in settings.
350
            if ($component->isDenied()) {
351
352
                $this->action(
353
                    self::ACTION_DENY,
354
                    $component->getDenyStatusCode()
355
                );
356
357
                return $this->result = self::RESPONSE_DENY;
358
            }
359
        }
360
361
        /*
362
        |--------------------------------------------------------------------------
363
        | Stage - Filters
364
        |--------------------------------------------------------------------------
365
        | This IP address is not listed in rule table, let's detect it.
366
        |
367
        */
368
369
        if (array_search(true, $this->filterStatus)) {
370
            return $this->result = $this->sessionHandler($this->filter());
371
        }
372
373
        return $this->result = $this->sessionHandler(self::RESPONSE_ALLOW);
374
    }
375
376
    /**
377
     * Start an action for this IP address, allow or deny, and give a reason for it.
378
     *
379
     * @param int    $actionCode - 0: deny, 1: allow, 9: unban.
380
     * @param string $reasonCode
381
     * @param string $assignIp
382
     * 
383
     * @return void
384
     */
385
    protected function action(int $actionCode, int $reasonCode, string $assignIp = ''): void
386
    {
387
        $ip = $this->ip;
388
        $rdns = $this->rdns;
389
        $now = time();
390
        $logData = [];
391
    
392
        if ('' !== $assignIp) {
393
            $ip = $assignIp;
394
            $rdns = gethostbyaddr($ip);
395
        }
396
397
        if ($actionCode === self::ACTION_UNBAN) {
398
            $this->driver->delete($ip, 'rule');
399
        } else {
400
            $logData['log_ip']     = $ip;
401
            $logData['ip_resolve'] = $rdns;
402
            $logData['time']       = $now;
403
            $logData['type']       = $actionCode;
404
            $logData['reason']     = $reasonCode;
405
            $logData['attempts']   = 0;
406
407
            $this->driver->save($ip, $logData, 'rule');
408
        }
409
410
        // Remove logs for this IP address because It already has it's own rule on system.
411
        // No need to count for it anymore.
412
        $this->driver->delete($ip, 'filter');
413
414
        // Log this action.
415
        $this->log($actionCode, $ip);
416
    }
417
418
    // @codeCoverageIgnoreEnd
419
420
    /*
421
    | -------------------------------------------------------------------
422
    |                            Public APIs
423
    | -------------------------------------------------------------------
424
    */
425
426
    /**
427
     * Set a captcha.
428
     *
429
     * @param CaptchaInterface $instance
430
     *
431
     * @return void
432
     */
433
    public function setCaptcha(CaptchaInterface $instance): void
434
    {
435
        $class = $this->getClassName($instance);
436
        $this->captcha[$class] = $instance;
437
    }
438
439
    /**
440
     * Set a data driver.
441
     *
442
     * @param DriverProvider $driver Query data from the driver you choose to use.
443
     *
444
     * @return void
445
     */
446
    public function setDriver(DriverProvider $driver): void
447
    {
448
        $this->driver = $driver;
449
    }
450
451
    /**
452
     * Set a action log logger.
453
     *
454
     * @param ActionLogger $logger
455
     *
456
     * @return void
457
     */
458
    public function setLogger(ActionLogger $logger): void
459
    {
460
        $this->logger = $logger;
461
    }
462
463
    /**
464
     * Set a messenger
465
     *
466
     * @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...
467
     *
468
     * @return void
469
     */
470
    public function setMessenger(MessengerInterface $instance): void
471
    {
472
        $class = $this->getClassName($instance);
473
        $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...
474
    }
475
476
    /**
477
     * Strict mode.
478
     * This option will take effects to all components.
479
     * 
480
     * @param bool $bool Set true to enble strict mode, false to disable it overwise.
481
     *
482
     * @return void
483
     */
484
    public function setStrict(bool $bool)
485
    {
486
        $this->strictMode = $bool;
487
    }
488
489
490
491
    /**
492
     * For first time installation only. This is for creating data tables automatically.
493
     * Turning it on will check the data tables exist or not at every single pageview, 
494
     * it's not good for high traffic websites.
495
     *
496
     * @param bool $bool
497
     * 
498
     * @return void
499
     */
500
    public function createDatabase(bool $bool)
501
    {
502
        $this->autoCreateDatabase = $bool;
503
    }
504
505
    /**
506
     * Set a data channel.
507
     *
508
     * This will create databases for the channel.
509
     *
510
     * @param string $channel Specify a channel.
511
     *
512
     * @return void
513
     */
514
    public function setChannel(string $channel)
515
    {
516
        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...
517
            throw new LogicException('setChannel method requires setDriver set first.');
518
        } else {
519
            $this->driver->setChannel($channel);
520
        }
521
    }
522
523
    /**
524
     * Return the result from Captchas.
525
     *
526
     * @return bool
527
     */
528
    public function captchaResponse(): bool
529
    {
530
        foreach ($this->captcha as $captcha) {
531
            
532
            if (!$captcha->response()) {
533
                return false;
534
            }
535
        }
536
537
        if (!empty($this->sessionLimit['count'])) {
538
            $this->result = $this->sessionHandler(self::RESPONSE_ALLOW);
539
        }
540
541
        return true;
542
    }
543
544
    /**
545
     * Ban an IP.
546
     *
547
     * @param string $ip A valid IP address.
548
     *
549
     * @return void
550
     */
551
    public function ban(string $ip = ''): void
552
    {
553
        if ('' === $ip) {
554
            $ip = $this->ip;
555
        }
556
 
557
        $this->action(
558
            self::ACTION_DENY,
559
            self::REASON_MANUAL_BAN,
560
            $ip
561
        );
562
    }
563
564
    /**
565
     * Unban an IP.
566
     *
567
     * @param string $ip A valid IP address.
568
     *
569
     * @return void
570
     */
571
    public function unban(string $ip = ''): void
572
    {
573
        if ('' === $ip) {
574
            $ip = $this->ip;
575
        }
576
577
        $this->action(
578
            self::ACTION_UNBAN,
579
            self::REASON_MANUAL_BAN,
580
            $ip
581
        );
582
        $this->log(self::ACTION_UNBAN);
583
584
        $this->result = self::RESPONSE_ALLOW;
585
    }
586
587
    /**
588
     * Set a property setting.
589
     *
590
     * @param string $key   The key of a property setting.
591
     * @param mixed  $value The value of a property setting.
592
     *
593
     * @return void
594
     */
595
    public function setProperty(string $key = '', $value = '')
596
    {
597
        if (isset($this->properties[$key])) {
598
            $this->properties[$key] = $value;
599
        }
600
    }
601
602
    /**
603
     * Set the property settings.
604
     * 
605
     * @param array $settings The settings.
606
     *
607
     * @return void
608
     */
609
    public function setProperties(array $settings): void
610
    {
611
        foreach (array_keys($this->properties) as $k) {
612
            if (isset($settings[$k])) {
613
                $this->properties[$k] = $settings[$k];
614
            }
615
        }
616
    }
617
618
    /**
619
     * Limt online sessions.
620
     *
621
     * @param int $count
622
     * @param int $period
623
     *
624
     * @return void
625
     */
626
    public function limitSession(int $count = 1000, int $period = 300): void
627
    {
628
        $this->sessionLimit = [
629
            'count' => $count,
630
            'period' => $period
631
        ];
632
    }
633
634
    /**
635
     * Customize the dialog UI.
636
     *
637
     * @return void
638
     */
639
    public function setDialogUI(array $settings): void
640
    {
641
        $this->dialogUI = $settings;
642
    }
643
644
    /**
645
     * Set the frontend template directory.
646
     *
647
     * @param string $directory
648
     *
649
     * @return void
650
     */
651
    public function setTemplateDirectory(string $directory)
652
    {
653
        if (!is_dir($directory)) {
654
            throw new InvalidArgumentException('The template directory does not exist.');
655
        }
656
        $this->templateDirectory = $directory;
657
    }
658
659
    /**
660
     * Get a template PHP file.
661
     *
662
     * @param string $type The template type.
663
     *
664
     * @return string
665
     */
666
    protected function getTemplate(string $type): string
667
    {
668
        $directory = self::KERNEL_DIR . '/../../templates/frontend';
669
670
        if (!empty($this->templateDirectory)) {
671
            $directory = $this->templateDirectory;
672
        }
673
674
        $path = $directory . '/' . $type . '.php';
675
676
        if (!file_exists($path)) {
677
            throw new RuntimeException(
678
                sprintf(
679
                    'The templeate file is missing. (%s)',
680
                    $path
681
                )
682
            );
683
        }
684
685
        return $path;
686
    }
687
688
    /**
689
     * Get a class name without namespace string.
690
     *
691
     * @param object $instance Class
692
     * 
693
     * @return void
694
     */
695
    protected function getClassName($instance): string
696
    {
697
        $class = get_class($instance);
698
        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...
699
    }
700
701
    /**
702
     * Respond the result.
703
     *
704
     * @return ResponseInterface
705
     */
706
    public function respond(): ResponseInterface
707
    {
708
        $response = get_response();
709
        $type = '';
710
711
        if (self::RESPONSE_TEMPORARILY_DENY === $this->result) {
712
            $type = 'captcha';
713
            $statusCode = 403; // Forbidden.
714
715
        } elseif (self::RESPONSE_LIMIT_SESSION === $this->result) {
716
            $type = 'session_limitation';
717
            $statusCode = 429; // Too Many Requests.
718
719
        } elseif (self::RESPONSE_DENY === $this->result) {
720
            $type = 'rejection';
721
            $statusCode = 400; // Bad request.
722
        }
723
724
        // Nothing happened. Return.
725
        if (empty($type)) {
726
            // @codeCoverageIgnoreStart
727
            return $response;
728
            // @codeCoverageIgnoreEnd
729
        }
730
731
        $viewPath = $this->getTemplate($type);
732
733
        // The language of output UI. It is used on views.
734
        $langCode = get_session()->get('shieldon_ui_lang') ?? 'en';
735
        // Show online session count. It is used on views.
736
        $showOnlineInformation = true;
737
        // Show user information such as IP, user-agent, device name.
738
        $showUserInformation = true;
739
740
        if (empty($this->properties['display_online_info'])) {
741
            $showOnlineInformation = false;
742
        }
743
744
        if (empty($this->properties['display_user_info'])) {
745
            $showUserInformation = false;
746
        }
747
748
        if ($showUserInformation) {
749
            $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...
750
            $dialoguserinfo['rdns'] = $this->rdns;
751
            $dialoguserinfo['user_agent'] = get_request()->getHeaderLine('user-agent');
752
        }
753
754
        $ui = [
755
            'background_image' => $this->dialogUI['background_image'] ?? '',
756
            'bg_color'         => $this->dialogUI['bg_color']         ?? '#ffffff',
757
            'header_bg_color'  => $this->dialogUI['header_bg_color']  ?? '#212531',
758
            'header_color'     => $this->dialogUI['header_color']     ?? '#ffffff',
759
            'shadow_opacity'   => $this->dialogUI['shadow_opacity']   ?? '0.2',
760
        ];
761
762
        if (!defined('SHIELDON_VIEW')) {
763
            define('SHIELDON_VIEW', true);
764
        }
765
766
        $css = require $this->getTemplate('css/default');
767
768
        ob_start();
769
        require $viewPath;
770
        $output = ob_get_contents();
771
        ob_end_clean();
772
773
        // Remove unused variable notices generated from PHP intelephense.
774
        unset(
775
            $css,
776
            $ui,
777
            $langCode,
778
            $showOnlineInformation,
779
            $showLineupInformation,
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $showLineupInformation seems to be never defined.
Loading history...
780
            $showUserInformation
781
        );
782
783
        $stream = $response->getBody();
784
        $stream->write($output);
785
        $stream->rewind();
786
787
        return $response->
788
            withHeader('X-Protected-By', 'shieldon.io')->
789
            withBody($stream)->
790
            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...
791
    }
792
793
    /**
794
     * Run, run, run!
795
     *
796
     * Check the rule tables first, if an IP address has been listed.
797
     * Call function filter() if an IP address is not listed in rule tables.
798
     *
799
     * @return 
800
     */
801
    public function run(): int
802
    {
803
        if (!isset($this->driver)) {
804
            throw new RuntimeException(
805
                'Must register at least one data driver.'
806
            );
807
        }
808
        
809
        // Ignore the excluded urls.
810
        if (!empty($this->excludedUrls)) {
811
            foreach ($this->excludedUrls as $url) {
812
                if (0 === strpos(get_request()->getUri()->getPath(), $url)) {
813
                    return $this->result = self::RESPONSE_ALLOW;
814
                }
815
            }
816
        }
817
818
        // Execute closure functions.
819
        foreach ($this->closures as $closure) {
820
            $closure();
821
        }
822
823
        $result = $this->process();
824
825
        if ($result !== self::RESPONSE_ALLOW) {
826
827
            // Current session did not pass the CAPTCHA, it is still stuck in CAPTCHA page.
828
            $actionCode = self::LOG_CAPTCHA;
829
830
            // If current session's respone code is RESPONSE_DENY, record it as `blacklist_count` in our logs.
831
            // It is stuck in warning page, not CAPTCHA.
832
            if ($result === self::RESPONSE_DENY) {
833
                $actionCode = self::LOG_BLACKLIST;
834
            }
835
836
            if ($result === self::RESPONSE_LIMIT_SESSION) {
837
                $actionCode = self::LOG_LIMIT;
838
            }
839
840
            $this->log($actionCode);
841
842
        } else {
843
844
            $this->log(self::LOG_PAGEVIEW);
845
        }
846
847
 
848
        if (!empty($this->msgBody)) {
849
 
850
            // @codeCoverageIgnoreStart
851
852
            try {
853
                foreach ($this->messenger as $messenger) {
854
                    $messenger->setTimeout(2);
855
                    $messenger->send($this->msgBody);
856
                }
857
            } catch (RuntimeException $e) {
858
                // Do not throw error, becasue the third-party services might be unavailable.
859
            }
860
861
            // @codeCoverageIgnoreEnd
862
        }
863
864
865
        return $result;
866
    }
867
868
    
869
870
871
872
    /**
873
     * Set the URLs you want them to be excluded them from protection.
874
     *
875
     * @param array $urls The list of URL want to be excluded.
876
     *
877
     * @return void
878
     */
879
    public function setExcludedUrls(array $urls = []): void
880
    {
881
        $this->excludedUrls = $urls;
882
    }
883
884
    /**
885
     * Set a closure function.
886
     *
887
     * @param string  $key     The name for the closure class.
888
     * @param Closure $closure An instance will be later called.
889
     *
890
     * @return void
891
     */
892
    public function setClosure(string $key, Closure $closure): void
893
    {
894
        $this->closures[$key] = $closure;
895
    }
896
897
    /**
898
     * Print a JavasSript snippet in your webpages.
899
     * 
900
     * This snippet generate cookie on client's browser,then we check the 
901
     * cookie to identify the client is a rebot or not.
902
     *
903
     * @return string
904
     */
905
    public function outputJsSnippet(): string
906
    {
907
        $tmpCookieName = $this->properties['cookie_name'];
908
        $tmpCookieDomain = $this->properties['cookie_domain'];
909
910
        if (empty($tmpCookieDomain) && get_request()->getHeaderLine('host')) {
911
            $tmpCookieDomain = get_request()->getHeaderLine('host');
912
        }
913
914
        $tmpCookieValue = $this->properties['cookie_value'];
915
916
        $jsString = '
917
            <script>
918
                var d = new Date();
919
                d.setTime(d.getTime()+(60*60*24*30));
920
                document.cookie = "' . $tmpCookieName . '=' . $tmpCookieValue . ';domain=.' . $tmpCookieDomain . ';expires="+d.toUTCString();
921
            </script>
922
        ';
923
924
        return $jsString;
925
    }
926
927
    /**
928
     * Get current visior's path.
929
     *
930
     * @return string
931
     */
932
    public function getCurrentUrl(): string
933
    {
934
        return get_request()->getUri()->getPath();
935
    }
936
937
    /**
938
     * Displayed on Firewall Panel, tell you current what type of current
939
     * configuration is used for.
940
     * 
941
     * @param string $type The type of configuration.
942
     *                     demo | managed | config
943
     *
944
     * @return void
945
     */
946
    public function managedBy(string $type = ''): void
947
    {
948
        if (in_array($type, ['managed', 'config', 'demo'])) {
949
            $this->firewallType = $type;
950
        }
951
    }
952
}
953