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

Kernel::outputJsSnippet()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 20
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 8
nc 2
nop 0
dl 0
loc 20
rs 10
c 1
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
     * Disable filters.
506
     */
507
    public function disableFilters(): void
508
    {
509
        $this->setFilters([
510
            'session'   => false,
511
            'cookie'    => false,
512
            'referer'   => false,
513
            'frequency' => false,
514
        ]);
515
    }
516
517
    /**
518
     * For first time installation only. This is for creating data tables automatically.
519
     * Turning it on will check the data tables exist or not at every single pageview, 
520
     * it's not good for high traffic websites.
521
     *
522
     * @param bool $bool
523
     * 
524
     * @return void
525
     */
526
    public function createDatabase(bool $bool)
527
    {
528
        $this->autoCreateDatabase = $bool;
529
    }
530
531
    /**
532
     * Set a data channel.
533
     *
534
     * This will create databases for the channel.
535
     *
536
     * @param string $channel Specify a channel.
537
     *
538
     * @return void
539
     */
540
    public function setChannel(string $channel)
541
    {
542
        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...
543
            throw new LogicException('setChannel method requires setDriver set first.');
544
        } else {
545
            $this->driver->setChannel($channel);
546
        }
547
    }
548
549
    /**
550
     * Return the result from Captchas.
551
     *
552
     * @return bool
553
     */
554
    public function captchaResponse(): bool
555
    {
556
        foreach ($this->captcha as $captcha) {
557
            
558
            if (!$captcha->response()) {
559
                return false;
560
            }
561
        }
562
563
        if (!empty($this->sessionLimit['count'])) {
564
            $this->result = $this->sessionHandler(self::RESPONSE_ALLOW);
565
        }
566
567
        return true;
568
    }
569
570
    /**
571
     * Ban an IP.
572
     *
573
     * @param string $ip A valid IP address.
574
     *
575
     * @return void
576
     */
577
    public function ban(string $ip = ''): void
578
    {
579
        if ('' === $ip) {
580
            $ip = $this->ip;
581
        }
582
 
583
        $this->action(
584
            self::ACTION_DENY,
585
            self::REASON_MANUAL_BAN, $ip
586
        );
587
    }
588
589
    /**
590
     * Unban an IP.
591
     *
592
     * @param string $ip A valid IP address.
593
     *
594
     * @return void
595
     */
596
    public function unban(string $ip = ''): void
597
    {
598
        if ('' === $ip) {
599
            $ip = $this->ip;
600
        }
601
602
        $this->action(
603
            self::ACTION_UNBAN,
604
            self::REASON_MANUAL_BAN, $ip
605
        );
606
        $this->log(self::ACTION_UNBAN);
607
608
        $this->result = self::RESPONSE_ALLOW;
609
    }
610
611
    /**
612
     * Set a property setting.
613
     *
614
     * @param string $key   The key of a property setting.
615
     * @param mixed  $value The value of a property setting.
616
     *
617
     * @return void
618
     */
619
    public function setProperty(string $key = '', $value = '')
620
    {
621
        if (isset($this->properties[$key])) {
622
            $this->properties[$key] = $value;
623
        }
624
    }
625
626
    /**
627
     * Set the property settings.
628
     * 
629
     * @param array $settings The settings.
630
     *
631
     * @return void
632
     */
633
    public function setProperties(array $settings): void
634
    {
635
        foreach (array_keys($this->properties) as $k) {
636
            if (isset($settings[$k])) {
637
                $this->properties[$k] = $settings[$k];
638
            }
639
        }
640
    }
641
642
    /**
643
     * Limt online sessions.
644
     *
645
     * @param int $count
646
     * @param int $period
647
     *
648
     * @return void
649
     */
650
    public function limitSession(int $count = 1000, int $period = 300): void
651
    {
652
        $this->sessionLimit = [
653
            'count' => $count,
654
            'period' => $period
655
        ];
656
    }
657
658
    /**
659
     * Customize the dialog UI.
660
     *
661
     * @return void
662
     */
663
    public function setDialogUI(array $settings): void
664
    {
665
        $this->dialogUI = $settings;
666
    }
667
668
    /**
669
     * Set the frontend template directory.
670
     *
671
     * @param string $directory
672
     *
673
     * @return void
674
     */
675
    public function setTemplateDirectory(string $directory)
676
    {
677
        if (!is_dir($directory)) {
678
            throw new InvalidArgumentException('The template directory does not exist.');
679
        }
680
        $this->templateDirectory = $directory;
681
    }
682
683
    /**
684
     * Get a template PHP file.
685
     *
686
     * @param string $type The template type.
687
     *
688
     * @return string
689
     */
690
    protected function getTemplate(string $type): string
691
    {
692
        $directory = self::KERNEL_DIR . '/../../templates/frontend';
693
694
        if (!empty($this->templateDirectory)) {
695
            $directory = $this->templateDirectory;
696
        }
697
698
        $path = $directory . '/' . $type . '.php';
699
700
        if (!file_exists($path)) {
701
            throw new RuntimeException(
702
                sprintf(
703
                    'The templeate file is missing. (%s)',
704
                    $path
705
                )
706
            );
707
        }
708
709
        return $path;
710
    }
711
712
    /**
713
     * Get a class name without namespace string.
714
     *
715
     * @param object $instance Class
716
     * 
717
     * @return void
718
     */
719
    protected function getClassName($instance): string
720
    {
721
        $class = get_class($instance);
722
        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...
723
    }
724
725
    /**
726
     * Respond the result.
727
     *
728
     * @return ResponseInterface
729
     */
730
    public function respond(): ResponseInterface
731
    {
732
        $response = get_response();
733
        $type = '';
734
735
        if (self::RESPONSE_TEMPORARILY_DENY === $this->result) {
736
            $type = 'captcha';
737
            $statusCode = 403; // Forbidden.
738
739
        } elseif (self::RESPONSE_LIMIT_SESSION === $this->result) {
740
            $type = 'session_limitation';
741
            $statusCode = 429; // Too Many Requests.
742
743
        } elseif (self::RESPONSE_DENY === $this->result) {
744
            $type = 'rejection';
745
            $statusCode = 400; // Bad request.
746
        }
747
748
        // Nothing happened. Return.
749
        if (empty($type)) {
750
            // @codeCoverageIgnoreStart
751
            return $response;
752
            // @codeCoverageIgnoreEnd
753
        }
754
755
        $viewPath = $this->getTemplate($type);
756
757
        // The language of output UI. It is used on views.
758
        $langCode = get_session()->get('shieldon_ui_lang') ?? 'en';
759
        // Show online session count. It is used on views.
760
        $showOnlineInformation = true;
761
        // Show user information such as IP, user-agent, device name.
762
        $showUserInformation = true;
763
764
        if (empty($this->properties['display_online_info'])) {
765
            $showOnlineInformation = false;
766
        }
767
768
        if (empty($this->properties['display_user_info'])) {
769
            $showUserInformation = false;
770
        }
771
772
        if ($showUserInformation) {
773
            $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...
774
            $dialoguserinfo['rdns'] = $this->rdns;
775
            $dialoguserinfo['user_agent'] = get_request()->getHeaderLine('user-agent');
776
        }
777
778
        $ui = [
779
            'background_image' => $this->dialogUI['background_image'] ?? '',
780
            'bg_color'         => $this->dialogUI['bg_color']         ?? '#ffffff',
781
            'header_bg_color'  => $this->dialogUI['header_bg_color']  ?? '#212531',
782
            'header_color'     => $this->dialogUI['header_color']     ?? '#ffffff',
783
            'shadow_opacity'   => $this->dialogUI['shadow_opacity']   ?? '0.2',
784
        ];
785
786
        if (!defined('SHIELDON_VIEW')) {
787
            define('SHIELDON_VIEW', true);
788
        }
789
790
        $css = require $this->getTemplate('css/default');
791
792
        ob_start();
793
        require $viewPath;
794
        $output = ob_get_contents();
795
        ob_end_clean();
796
797
        // Remove unused variable notices generated from PHP intelephense.
798
        unset(
799
            $css,
800
            $ui,
801
            $langCode,
802
            $showOnlineInformation,
803
            $showLineupInformation,
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $showLineupInformation seems to be never defined.
Loading history...
804
            $showUserInformation
805
        );
806
807
        $stream = $response->getBody();
808
        $stream->write($output);
809
        $stream->rewind();
810
811
        return $response->
812
            withHeader('X-Protected-By', 'shieldon.io')->
813
            withBody($stream)->
814
            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...
815
    }
816
817
    /**
818
     * Run, run, run!
819
     *
820
     * Check the rule tables first, if an IP address has been listed.
821
     * Call function filter() if an IP address is not listed in rule tables.
822
     *
823
     * @return 
824
     */
825
    public function run(): int
826
    {
827
        if (!isset($this->driver)) {
828
            throw new RuntimeException(
829
                'Must register at least one data driver.'
830
            );
831
        }
832
        
833
        // Ignore the excluded urls.
834
        if (!empty($this->excludedUrls)) {
835
            foreach ($this->excludedUrls as $url) {
836
                if (0 === strpos(get_request()->getUri()->getPath(), $url)) {
837
                    return $this->result = self::RESPONSE_ALLOW;
838
                }
839
            }
840
        }
841
842
        // Execute closure functions.
843
        foreach ($this->closures as $closure) {
844
            $closure();
845
        }
846
847
        $result = $this->process();
848
849
        if ($result !== self::RESPONSE_ALLOW) {
850
851
            // Current session did not pass the CAPTCHA, it is still stuck in CAPTCHA page.
852
            $actionCode = self::LOG_CAPTCHA;
853
854
            // If current session's respone code is RESPONSE_DENY, record it as `blacklist_count` in our logs.
855
            // It is stuck in warning page, not CAPTCHA.
856
            if ($result === self::RESPONSE_DENY) {
857
                $actionCode = self::LOG_BLACKLIST;
858
            }
859
860
            if ($result === self::RESPONSE_LIMIT_SESSION) {
861
                $actionCode = self::LOG_LIMIT;
862
            }
863
864
            $this->log($actionCode);
865
866
        } else {
867
868
            $this->log(self::LOG_PAGEVIEW);
869
        }
870
871
 
872
        if (!empty($this->msgBody)) {
873
 
874
            // @codeCoverageIgnoreStart
875
876
            try {
877
                foreach ($this->messenger as $messenger) {
878
                    $messenger->setTimeout(2);
879
                    $messenger->send($this->msgBody);
880
                }
881
            } catch (RuntimeException $e) {
882
                // Do not throw error, becasue the third-party services might be unavailable.
883
            }
884
885
            // @codeCoverageIgnoreEnd
886
        }
887
888
889
        return $result;
890
    }
891
892
    /**
893
     * Set the filters.
894
     *
895
     * @param array $settings filter settings.
896
     *
897
     * @return void
898
     */
899
    public function setFilters(array $settings)
900
    {
901
        foreach (array_keys($this->filterStatus) as $k) {
902
            if (isset($settings[$k])) {
903
                $this->filterStatus[$k] = $settings[$k] ?? false;
904
            }
905
        }
906
    }
907
908
    /**
909
     * Set a filter.
910
     *
911
     * @param string $filterName The filter's name.
912
     * @param bool   $value      True for enabling the filter, overwise.
913
     *
914
     * @return void
915
     */
916
    public function setFilter(string $filterName, bool $value): void
917
    {
918
        if (isset($this->filterStatus[$filterName])) {
919
            $this->filterStatus[$filterName] = $value;
920
        }
921
    }
922
923
924
925
    /**
926
     * Set the URLs you want them to be excluded them from protection.
927
     *
928
     * @param array $urls The list of URL want to be excluded.
929
     *
930
     * @return void
931
     */
932
    public function setExcludedUrls(array $urls = []): void
933
    {
934
        $this->excludedUrls = $urls;
935
    }
936
937
    /**
938
     * Set a closure function.
939
     *
940
     * @param string  $key     The name for the closure class.
941
     * @param Closure $closure An instance will be later called.
942
     *
943
     * @return void
944
     */
945
    public function setClosure(string $key, Closure $closure): void
946
    {
947
        $this->closures[$key] = $closure;
948
    }
949
950
    /**
951
     * Print a JavasSript snippet in your webpages.
952
     * 
953
     * This snippet generate cookie on client's browser,then we check the 
954
     * cookie to identify the client is a rebot or not.
955
     *
956
     * @return string
957
     */
958
    public function outputJsSnippet(): string
959
    {
960
        $tmpCookieName = $this->properties['cookie_name'];
961
        $tmpCookieDomain = $this->properties['cookie_domain'];
962
963
        if (empty($tmpCookieDomain) && get_request()->getHeaderLine('host')) {
964
            $tmpCookieDomain = get_request()->getHeaderLine('host');
965
        }
966
967
        $tmpCookieValue = $this->properties['cookie_value'];
968
969
        $jsString = '
970
            <script>
971
                var d = new Date();
972
                d.setTime(d.getTime()+(60*60*24*30));
973
                document.cookie = "' . $tmpCookieName . '=' . $tmpCookieValue . ';domain=.' . $tmpCookieDomain . ';expires="+d.toUTCString();
974
            </script>
975
        ';
976
977
        return $jsString;
978
    }
979
980
    /**
981
     * Get current visior's path.
982
     *
983
     * @return string
984
     */
985
    public function getCurrentUrl(): string
986
    {
987
        return get_request()->getUri()->getPath();
988
    }
989
990
    /**
991
     * Displayed on Firewall Panel, tell you current what type of current
992
     * configuration is used for.
993
     * 
994
     * @param string $type The type of configuration.
995
     *                     demo | managed | config
996
     *
997
     * @return void
998
     */
999
    public function managedBy(string $type = ''): void
1000
    {
1001
        if (in_array($type, ['managed', 'config', 'demo'])) {
1002
            $this->firewallType = $type;
1003
        }
1004
    }
1005
}
1006