Passed
Push — v1 ( b0cc84...bf5890 )
by Andrew
07:47 queued 04:32
created

MetricsController::rateLimited()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 13
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 9
dl 0
loc 13
rs 9.9666
c 0
b 0
f 0
cc 3
nc 2
nop 0
1
<?php
2
/**
3
 * Webperf plugin for Craft CMS 3.x
4
 *
5
 * Monitor the performance of your webpages through real-world user timing data
6
 *
7
 * @link      https://nystudio107.com
0 ignored issues
show
Coding Style introduced by
The tag in position 1 should be the @copyright tag
Loading history...
8
 * @copyright Copyright (c) 2018 nystudio107
0 ignored issues
show
Coding Style introduced by
@copyright tag must contain a year and the name of the copyright holder
Loading history...
9
 */
0 ignored issues
show
Coding Style introduced by
PHP version not specified
Loading history...
Coding Style introduced by
Missing @category tag in file comment
Loading history...
Coding Style introduced by
Missing @package tag in file comment
Loading history...
Coding Style introduced by
Missing @author tag in file comment
Loading history...
Coding Style introduced by
Missing @license tag in file comment
Loading history...
10
11
namespace {
12
13
    require_once __DIR__.'/../lib/geoiploc.php';
0 ignored issues
show
Coding Style introduced by
File is being conditionally included; use "include_once" instead
Loading history...
14
}
15
16
namespace nystudio107\webperf\controllers {
17
18
    use nystudio107\webperf\helpers\MultiSite;
19
    use nystudio107\webperf\Webperf;
20
    use nystudio107\webperf\models\DataSample;
21
22
    use Jaybizzle\CrawlerDetect\CrawlerDetect;
0 ignored issues
show
Bug introduced by
The type Jaybizzle\CrawlerDetect\CrawlerDetect 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...
23
    use WhichBrowser\Parser;
0 ignored issues
show
Bug introduced by
The type WhichBrowser\Parser 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...
24
25
    use Craft;
0 ignored issues
show
Bug introduced by
The type Craft 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...
26
    use craft\errors\SiteNotFoundException;
0 ignored issues
show
Bug introduced by
The type craft\errors\SiteNotFoundException 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...
27
    use craft\helpers\UrlHelper;
0 ignored issues
show
Bug introduced by
The type craft\helpers\UrlHelper 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...
28
    use craft\web\Controller;
0 ignored issues
show
Bug introduced by
The type craft\web\Controller 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...
29
30
    use yii\base\InvalidConfigException;
0 ignored issues
show
Bug introduced by
The type yii\base\InvalidConfigException 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...
31
32
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
33
     * @author    nystudio107
0 ignored issues
show
Coding Style introduced by
The tag in position 1 should be the @package tag
Loading history...
Coding Style introduced by
Content of the @author tag must be in the form "Display Name <[email protected]>"
Loading history...
Coding Style introduced by
Tag value indented incorrectly; expected 2 spaces but found 4
Loading history...
34
     * @package   Webperf
0 ignored issues
show
Coding Style introduced by
Tag value indented incorrectly; expected 1 spaces but found 3
Loading history...
35
     * @since     1.0.0
0 ignored issues
show
Coding Style introduced by
The tag in position 3 should be the @author tag
Loading history...
Coding Style introduced by
Tag value indented incorrectly; expected 3 spaces but found 5
Loading history...
36
     */
0 ignored issues
show
Coding Style introduced by
Missing @category tag in class comment
Loading history...
Coding Style introduced by
Missing @license tag in class comment
Loading history...
Coding Style introduced by
Missing @link tag in class comment
Loading history...
37
    class MetricsController extends Controller
38
    {
39
        // Constants
40
        // =========================================================================
41
42
        const LAST_BEACON_CACHE_KEY = 'webperf-last-beacon';
43
44
        // Public Properties
45
        // =========================================================================
46
47
        public $enableCsrfValidation = false;
48
49
        // Protected Properties
50
        // =========================================================================
51
52
        /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
53
         * @var    bool|array Allows anonymous access to this controller's
54
         *         actions. The actions must be in 'kebab-case'
55
         * @access protected
56
         */
57
        protected $allowAnonymous = ['beacon'];
58
59
60
        // Public Methods
61
        // =========================================================================
62
63
        /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
64
         * @return void
65
         * @throws \yii\base\ExitException
66
         */
67
        public function actionBeacon()
68
        {
69
            // Rate limit the beacon sampling
70
            if ($this->rateLimited()) {
71
                Craft::$app->end();
72
            }
73
            // Get the incoming params from the beacon
74
            try {
75
                $params = Craft::$app->getRequest()->getBodyParams();
76
            } catch (InvalidConfigException $e) {
77
                $params = [];
78
            }
79
            // Ensure the beacon has at least the URL parameter
80
            if (empty($params) || empty($params['u'])) {
81
                Craft::$app->end();
82
            }
83
            // This parameter will exist (but have no value) if the beacon was
84
            // fired as part of the onbeforeunload event.
85
            if (isset($params['rt_quit'])) {
86
                Craft::$app->end();
87
            }
88
            // Filter out bot/spam requests via UserAgent
89
            if (Webperf::$settings->filterBotUserAgents) {
90
                $crawlerDetect = new CrawlerDetect;
91
                // Check the user agent of the current 'visitor'
92
                if ($crawlerDetect->isCrawler()) {
93
                    Craft::$app->end();
94
                }
95
            }
96
            // Allocate a new DataSample, and fill it in
97
            $sample = new DataSample();
98
            $url = $params['u'];
99
            $sample->url = UrlHelper::stripQueryString($url);
100
            $sample->queryString = parse_url($url, PHP_URL_QUERY);
101
            // Get the site id
102
            try {
103
                $site = MultiSite::getSiteFromUrl($sample->url);
104
                $sample->siteId = $site->id;
105
            } catch (SiteNotFoundException $e) {
106
                $sample->siteId = null;
107
            }
108
            // Fill in all of the timing information that's available
109
            $sample->pageLoad = $params['t_done'] ?? null;
110
            if (!empty($params['nt_dns_end']) && !empty($params['nt_dns_st'])) {
111
                $sample->dns = $params['nt_dns_end'] - $params['nt_dns_st'];
112
                // If there was no delay, set it to null
113
                if ($sample->dns === 0) {
114
                    $sample->dns = null;
115
                }
116
            }
117
            if (!empty($params['nt_con_end']) && !empty($params['nt_con_st'])) {
118
                $sample->connect = $params['nt_con_end'] - $params['nt_con_st'];
119
                // If there was no delay, set it to null
120
                if ($sample->connect === 0) {
121
                    $sample->connect = null;
122
                }
123
            }
124
            if (!empty($params['t_resp'])) {
125
                $sample->firstByte = $params['t_resp'];
126
            }
127
            if (!empty($params['pt_fp'])) {
128
                $sample->firstPaint = $params['pt_fp'];
129
            }
130
            if (!empty($params['pt_fcp'])) {
131
                $sample->firstContentfulPaint = $params['pt_fcp'];
132
            }
133
            if (!empty($params['nt_domint']) && !empty($params['nt_nav_st'])) {
134
                $sample->domInteractive = $params['nt_domint'] - $params['nt_nav_st'];
135
            }
136
            if (!empty($params['nt_domcomp']) && !empty($params['nt_nav_st'])) {
137
                $sample->pageLoad = $params['nt_domcomp'] - $params['nt_nav_st'];
138
            }
139
            // Set the document title
140
            if (!empty($params['doc_title'])) {
141
                $sample->title = $params['doc_title'];
142
            }
143
            // Set the request id
144
            $sample->requestId = Webperf::$requestUuid;
145
            if (!empty($params['request_id'])) {
146
                $sample->requestId = $params['request_id'];
147
            }
148
            // Fill in information from the current request
149
            $request = Craft::$app->getRequest();
150
            $ip = $request->userIP;
151
            if ($ip) {
152
                $sample->countryCode = getCountryFromIP($ip);
0 ignored issues
show
Bug introduced by
The function getCountryFromIP was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

152
                $sample->countryCode = /** @scrutinizer ignore-call */ getCountryFromIP($ip);
Loading history...
153
                // getCountryFromIP returns 'ZZ' for unknown countries, map to '??'
154
                if ($sample->countryCode === 'ZZ') {
155
                    $sample->countryCode = '??';
156
                }
157
            }
158
            $userAgent = $request->userAgent;
159
            if ($userAgent) {
160
                $parser = new Parser($userAgent);
161
                $sample->device = $parser->device->model;
162
                $sample->browser = $parser->browser->name;
163
                $sample->os = $parser->os->name;
164
                $sample->mobile = $parser->isMobile();
165
            }
166
            // Save the data sample
167
            $sample->setScenario(DataSample::SCENARIO_BOOMERANG_BEACON);
168
            Craft::debug('Saving DataSample: '.print_r($sample, true), __METHOD__);
169
            Webperf::$plugin->dataSamples->addDataSample($sample);
170
            Craft::$app->end();
171
        }
172
173
        // Protected Methods
174
        // =========================================================================
175
176
        /**
177
         * Don't allow a DDOS attack on the beacon endpoint by rate limiting the
178
         * data sample recording
179
         *
180
         * @return bool
181
         */
182
        protected function rateLimited(): bool
183
        {
184
            $limited = false;
185
            $now = round(microtime(true) * 1000);
186
            $cache = Craft::$app->getCache();
187
            $then = $cache->get(self::LAST_BEACON_CACHE_KEY);
188
            if (($then !== false) && ($now - (int)$then < Webperf::$settings->rateLimitMs)) {
189
                $limited = true;
190
                Craft::warning('Beacon ignored due to rate limiting', __METHOD__);
191
            }
192
            $cache->set(self::LAST_BEACON_CACHE_KEY, $now, 0);
193
194
            return $limited;
195
        }
196
    }
197
}
198