Passed
Push — 2.x ( 8d739d...192121 )
by Terry
02:09
created

Kernel::disableFilters()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 5
nc 1
nop 0
dl 0
loc 7
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): void
279
    {
280
        if (!$this->logger) {
281
            return;
282
        }
283
284
        $logData = [];
285
        $logData['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
                // @since 0.1.8
353
                $this->action(
354
                    self::ACTION_DENY,
355
                    $component->getDenyStatusCode()
356
                );
357
358
                return $this->result = self::RESPONSE_DENY;
359
            }
360
        }
361
362
        /*
363
        |--------------------------------------------------------------------------
364
        | Stage - Filters
365
        |--------------------------------------------------------------------------
366
        | This IP address is not listed in rule table, let's detect it.
367
        |
368
        */
369
370
        if (array_search(true, $this->filterStatus)) {
371
            return $this->result = $this->sessionHandler($this->filter());
372
        }
373
374
        return $this->result = $this->sessionHandler(self::RESPONSE_ALLOW);
375
    }
376
377
    /**
378
     * Start an action for this IP address, allow or deny, and give a reason for it.
379
     *
380
     * @param int    $actionCode - 0: deny, 1: allow, 9: unban.
381
     * @param string $reasonCode
382
     * @param string $assignIp
383
     * 
384
     * @return void
385
     */
386
    protected function action(int $actionCode, int $reasonCode, string $assignIp = ''): void
387
    {
388
        $ip = $this->ip;
389
        $rdns = $this->rdns;
390
        $now = time();
391
        $logData = [];
392
    
393
        if ('' !== $assignIp) {
394
            $ip = $assignIp;
395
            $rdns = gethostbyaddr($ip);
396
        }
397
398
        switch ($actionCode) {
399
            case self::ACTION_ALLOW: // acutally not used.
400
            case self::ACTION_DENY:  // actually not used.
401
            case self::ACTION_TEMPORARILY_DENY:
402
                $logData['log_ip']     = $ip;
403
                $logData['ip_resolve'] = $rdns;
404
                $logData['time']       = $now;
405
                $logData['type']       = $actionCode;
406
                $logData['reason']     = $reasonCode;
407
                $logData['attempts']   = 0;
408
409
                $this->driver->save($ip, $logData, 'rule');
410
                break;
411
            
412
            case self::ACTION_UNBAN:
413
                $this->driver->delete($ip, 'rule');
414
                break;
415
        }
416
417
        // Remove logs for this IP address because It already has it's own rule on system.
418
        // No need to count it anymore.
419
        $this->driver->delete($ip, 'filter');
420
421
        if (null !== $this->logger) {
422
            $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...
423
            $log['session_id']  = get_session()->get('id');
424
            $log['action_code'] = $actionCode;
425
            $log['timesamp']    = $now;
426
427
            $this->logger->add($log);
428
        }
429
    }
430
431
    // @codeCoverageIgnoreEnd
432
433
    /*
434
    | -------------------------------------------------------------------
435
    |                            Public APIs
436
    | -------------------------------------------------------------------
437
    */
438
439
440
441
    /**
442
     * Set a captcha.
443
     *
444
     * @param CaptchaInterface $instance
445
     *
446
     * @return void
447
     */
448
    public function setCaptcha(CaptchaInterface $instance): void
449
    {
450
        $class = $this->getClassName($instance);
451
        $this->captcha[$class] = $instance;
452
    }
453
454
    /**
455
     * Set a data driver.
456
     *
457
     * @param DriverProvider $driver Query data from the driver you choose to use.
458
     *
459
     * @return void
460
     */
461
    public function setDriver(DriverProvider $driver): void
462
    {
463
        $this->driver = $driver;
464
    }
465
466
    /**
467
     * Set a action log logger.
468
     *
469
     * @param ActionLogger $logger
470
     *
471
     * @return void
472
     */
473
    public function setLogger(ActionLogger $logger): void
474
    {
475
        $this->logger = $logger;
476
    }
477
478
    /**
479
     * Set a messenger
480
     *
481
     * @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...
482
     *
483
     * @return void
484
     */
485
    public function setMessenger(MessengerInterface $instance): void
486
    {
487
        $class = $this->getClassName($instance);
488
        $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...
489
    }
490
491
    /**
492
     * Strict mode.
493
     * This option will take effects to all components.
494
     * 
495
     * @param bool $bool Set true to enble strict mode, false to disable it overwise.
496
     *
497
     * @return void
498
     */
499
    public function setStrict(bool $bool)
500
    {
501
        $this->strictMode = $bool;
502
    }
503
504
505
506
    /**
507
     * For first time installation only. This is for creating data tables automatically.
508
     * Turning it on will check the data tables exist or not at every single pageview, 
509
     * it's not good for high traffic websites.
510
     *
511
     * @param bool $bool
512
     * 
513
     * @return void
514
     */
515
    public function createDatabase(bool $bool)
516
    {
517
        $this->autoCreateDatabase = $bool;
518
    }
519
520
    /**
521
     * Set a data channel.
522
     *
523
     * This will create databases for the channel.
524
     *
525
     * @param string $channel Specify a channel.
526
     *
527
     * @return void
528
     */
529
    public function setChannel(string $channel)
530
    {
531
        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...
532
            throw new LogicException('setChannel method requires setDriver set first.');
533
        } else {
534
            $this->driver->setChannel($channel);
535
        }
536
    }
537
538
    /**
539
     * Return the result from Captchas.
540
     *
541
     * @return bool
542
     */
543
    public function captchaResponse(): bool
544
    {
545
        foreach ($this->captcha as $captcha) {
546
            
547
            if (!$captcha->response()) {
548
                return false;
549
            }
550
        }
551
552
        if (!empty($this->sessionLimit['count'])) {
553
            $this->result = $this->sessionHandler(self::RESPONSE_ALLOW);
554
        }
555
556
        return true;
557
    }
558
559
    /**
560
     * Ban an IP.
561
     *
562
     * @param string $ip A valid IP address.
563
     *
564
     * @return void
565
     */
566
    public function ban(string $ip = ''): void
567
    {
568
        if ('' === $ip) {
569
            $ip = $this->ip;
570
        }
571
 
572
        $this->action(
573
            self::ACTION_DENY,
574
            self::REASON_MANUAL_BAN, $ip
575
        );
576
    }
577
578
    /**
579
     * Unban an IP.
580
     *
581
     * @param string $ip A valid IP address.
582
     *
583
     * @return void
584
     */
585
    public function unban(string $ip = ''): void
586
    {
587
        if ('' === $ip) {
588
            $ip = $this->ip;
589
        }
590
591
        $this->action(
592
            self::ACTION_UNBAN,
593
            self::REASON_MANUAL_BAN, $ip
594
        );
595
        $this->log(self::ACTION_UNBAN);
596
597
        $this->result = self::RESPONSE_ALLOW;
598
    }
599
600
    /**
601
     * Set a property setting.
602
     *
603
     * @param string $key   The key of a property setting.
604
     * @param mixed  $value The value of a property setting.
605
     *
606
     * @return void
607
     */
608
    public function setProperty(string $key = '', $value = '')
609
    {
610
        if (isset($this->properties[$key])) {
611
            $this->properties[$key] = $value;
612
        }
613
    }
614
615
    /**
616
     * Set the property settings.
617
     * 
618
     * @param array $settings The settings.
619
     *
620
     * @return void
621
     */
622
    public function setProperties(array $settings): void
623
    {
624
        foreach (array_keys($this->properties) as $k) {
625
            if (isset($settings[$k])) {
626
                $this->properties[$k] = $settings[$k];
627
            }
628
        }
629
    }
630
631
    /**
632
     * Limt online sessions.
633
     *
634
     * @param int $count
635
     * @param int $period
636
     *
637
     * @return void
638
     */
639
    public function limitSession(int $count = 1000, int $period = 300): void
640
    {
641
        $this->sessionLimit = [
642
            'count' => $count,
643
            'period' => $period
644
        ];
645
    }
646
647
    /**
648
     * Customize the dialog UI.
649
     *
650
     * @return void
651
     */
652
    public function setDialogUI(array $settings): void
653
    {
654
        $this->dialogUI = $settings;
655
    }
656
657
    /**
658
     * Set the frontend template directory.
659
     *
660
     * @param string $directory
661
     *
662
     * @return void
663
     */
664
    public function setTemplateDirectory(string $directory)
665
    {
666
        if (!is_dir($directory)) {
667
            throw new InvalidArgumentException('The template directory does not exist.');
668
        }
669
        $this->templateDirectory = $directory;
670
    }
671
672
    /**
673
     * Get a template PHP file.
674
     *
675
     * @param string $type The template type.
676
     *
677
     * @return string
678
     */
679
    protected function getTemplate(string $type): string
680
    {
681
        $directory = self::KERNEL_DIR . '/../../templates/frontend';
682
683
        if (!empty($this->templateDirectory)) {
684
            $directory = $this->templateDirectory;
685
        }
686
687
        $path = $directory . '/' . $type . '.php';
688
689
        if (!file_exists($path)) {
690
            throw new RuntimeException(
691
                sprintf(
692
                    'The templeate file is missing. (%s)',
693
                    $path
694
                )
695
            );
696
        }
697
698
        return $path;
699
    }
700
701
    /**
702
     * Get a class name without namespace string.
703
     *
704
     * @param object $instance Class
705
     * 
706
     * @return void
707
     */
708
    protected function getClassName($instance): string
709
    {
710
        $class = get_class($instance);
711
        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...
712
    }
713
714
    /**
715
     * Respond the result.
716
     *
717
     * @return ResponseInterface
718
     */
719
    public function respond(): ResponseInterface
720
    {
721
        $response = get_response();
722
        $type = '';
723
724
        if (self::RESPONSE_TEMPORARILY_DENY === $this->result) {
725
            $type = 'captcha';
726
            $statusCode = 403; // Forbidden.
727
728
        } elseif (self::RESPONSE_LIMIT_SESSION === $this->result) {
729
            $type = 'session_limitation';
730
            $statusCode = 429; // Too Many Requests.
731
732
        } elseif (self::RESPONSE_DENY === $this->result) {
733
            $type = 'rejection';
734
            $statusCode = 400; // Bad request.
735
        }
736
737
        // Nothing happened. Return.
738
        if (empty($type)) {
739
            // @codeCoverageIgnoreStart
740
            return $response;
741
            // @codeCoverageIgnoreEnd
742
        }
743
744
        $viewPath = $this->getTemplate($type);
745
746
        // The language of output UI. It is used on views.
747
        $langCode = get_session()->get('shieldon_ui_lang') ?? 'en';
748
        // Show online session count. It is used on views.
749
        $showOnlineInformation = true;
750
        // Show user information such as IP, user-agent, device name.
751
        $showUserInformation = true;
752
753
        if (empty($this->properties['display_online_info'])) {
754
            $showOnlineInformation = false;
755
        }
756
757
        if (empty($this->properties['display_user_info'])) {
758
            $showUserInformation = false;
759
        }
760
761
        if ($showUserInformation) {
762
            $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...
763
            $dialoguserinfo['rdns'] = $this->rdns;
764
            $dialoguserinfo['user_agent'] = get_request()->getHeaderLine('user-agent');
765
        }
766
767
        $ui = [
768
            'background_image' => $this->dialogUI['background_image'] ?? '',
769
            'bg_color'         => $this->dialogUI['bg_color']         ?? '#ffffff',
770
            'header_bg_color'  => $this->dialogUI['header_bg_color']  ?? '#212531',
771
            'header_color'     => $this->dialogUI['header_color']     ?? '#ffffff',
772
            'shadow_opacity'   => $this->dialogUI['shadow_opacity']   ?? '0.2',
773
        ];
774
775
        if (!defined('SHIELDON_VIEW')) {
776
            define('SHIELDON_VIEW', true);
777
        }
778
779
        $css = require $this->getTemplate('css/default');
780
781
        ob_start();
782
        require $viewPath;
783
        $output = ob_get_contents();
784
        ob_end_clean();
785
786
        // Remove unused variable notices generated from PHP intelephense.
787
        unset(
788
            $css,
789
            $ui,
790
            $langCode,
791
            $showOnlineInformation,
792
            $showLineupInformation,
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $showLineupInformation seems to be never defined.
Loading history...
793
            $showUserInformation
794
        );
795
796
        $stream = $response->getBody();
797
        $stream->write($output);
798
        $stream->rewind();
799
800
        return $response->
801
            withHeader('X-Protected-By', 'shieldon.io')->
802
            withBody($stream)->
803
            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...
804
    }
805
806
    /**
807
     * Run, run, run!
808
     *
809
     * Check the rule tables first, if an IP address has been listed.
810
     * Call function filter() if an IP address is not listed in rule tables.
811
     *
812
     * @return 
813
     */
814
    public function run(): int
815
    {
816
        if (!isset($this->driver)) {
817
            throw new RuntimeException(
818
                'Must register at least one data driver.'
819
            );
820
        }
821
        
822
        // Ignore the excluded urls.
823
        if (!empty($this->excludedUrls)) {
824
            foreach ($this->excludedUrls as $url) {
825
                if (0 === strpos(get_request()->getUri()->getPath(), $url)) {
826
                    return $this->result = self::RESPONSE_ALLOW;
827
                }
828
            }
829
        }
830
831
        // Execute closure functions.
832
        foreach ($this->closures as $closure) {
833
            $closure();
834
        }
835
836
        $result = $this->process();
837
838
        if ($result !== self::RESPONSE_ALLOW) {
839
840
            // Current session did not pass the CAPTCHA, it is still stuck in CAPTCHA page.
841
            $actionCode = self::LOG_CAPTCHA;
842
843
            // If current session's respone code is RESPONSE_DENY, record it as `blacklist_count` in our logs.
844
            // It is stuck in warning page, not CAPTCHA.
845
            if ($result === self::RESPONSE_DENY) {
846
                $actionCode = self::LOG_BLACKLIST;
847
            }
848
849
            if ($result === self::RESPONSE_LIMIT_SESSION) {
850
                $actionCode = self::LOG_LIMIT;
851
            }
852
853
            $this->log($actionCode);
854
855
        } else {
856
857
            $this->log(self::LOG_PAGEVIEW);
858
        }
859
860
 
861
        if (!empty($this->msgBody)) {
862
 
863
            // @codeCoverageIgnoreStart
864
865
            try {
866
                foreach ($this->messenger as $messenger) {
867
                    $messenger->setTimeout(2);
868
                    $messenger->send($this->msgBody);
869
                }
870
            } catch (RuntimeException $e) {
871
                // Do not throw error, becasue the third-party services might be unavailable.
872
            }
873
874
            // @codeCoverageIgnoreEnd
875
        }
876
877
878
        return $result;
879
    }
880
881
    
882
883
884
885
    /**
886
     * Set the URLs you want them to be excluded them from protection.
887
     *
888
     * @param array $urls The list of URL want to be excluded.
889
     *
890
     * @return void
891
     */
892
    public function setExcludedUrls(array $urls = []): void
893
    {
894
        $this->excludedUrls = $urls;
895
    }
896
897
    /**
898
     * Set a closure function.
899
     *
900
     * @param string  $key     The name for the closure class.
901
     * @param Closure $closure An instance will be later called.
902
     *
903
     * @return void
904
     */
905
    public function setClosure(string $key, Closure $closure): void
906
    {
907
        $this->closures[$key] = $closure;
908
    }
909
910
    /**
911
     * Print a JavasSript snippet in your webpages.
912
     * 
913
     * This snippet generate cookie on client's browser,then we check the 
914
     * cookie to identify the client is a rebot or not.
915
     *
916
     * @return string
917
     */
918
    public function outputJsSnippet(): string
919
    {
920
        $tmpCookieName = $this->properties['cookie_name'];
921
        $tmpCookieDomain = $this->properties['cookie_domain'];
922
923
        if (empty($tmpCookieDomain) && get_request()->getHeaderLine('host')) {
924
            $tmpCookieDomain = get_request()->getHeaderLine('host');
925
        }
926
927
        $tmpCookieValue = $this->properties['cookie_value'];
928
929
        $jsString = '
930
            <script>
931
                var d = new Date();
932
                d.setTime(d.getTime()+(60*60*24*30));
933
                document.cookie = "' . $tmpCookieName . '=' . $tmpCookieValue . ';domain=.' . $tmpCookieDomain . ';expires="+d.toUTCString();
934
            </script>
935
        ';
936
937
        return $jsString;
938
    }
939
940
    /**
941
     * Get current visior's path.
942
     *
943
     * @return string
944
     */
945
    public function getCurrentUrl(): string
946
    {
947
        return get_request()->getUri()->getPath();
948
    }
949
950
    /**
951
     * Displayed on Firewall Panel, tell you current what type of current
952
     * configuration is used for.
953
     * 
954
     * @param string $type The type of configuration.
955
     *                     demo | managed | config
956
     *
957
     * @return void
958
     */
959
    public function managedBy(string $type = ''): void
960
    {
961
        if (in_array($type, ['managed', 'config', 'demo'])) {
962
            $this->firewallType = $type;
963
        }
964
    }
965
}
966