Passed
Push — 2.x ( ed3db8...f88ed3 )
by Terry
01:57
created

Kernel::getClassName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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