1 | <?php |
||||
2 | /** |
||||
3 | * Retour plugin for Craft CMS 3.x |
||||
4 | * |
||||
5 | * Retour allows you to intelligently redirect legacy URLs, so that you don't |
||||
6 | * lose SEO value when rebuilding & restructuring a website |
||||
7 | * |
||||
8 | * @link https://nystudio107.com/ |
||||
0 ignored issues
–
show
Coding Style
introduced
by
![]() |
|||||
9 | * @copyright Copyright (c) 2018 nystudio107 |
||||
0 ignored issues
–
show
|
|||||
10 | */ |
||||
0 ignored issues
–
show
|
|||||
11 | |||||
12 | namespace nystudio107\retour\services; |
||||
13 | |||||
14 | use nystudio107\retour\Retour; |
||||
15 | |||||
16 | use nystudio107\retour\models\Stats as StatsModel; |
||||
17 | |||||
18 | use Craft; |
||||
19 | use craft\base\Component; |
||||
20 | use craft\db\Query; |
||||
21 | use craft\helpers\Db; |
||||
22 | use craft\helpers\UrlHelper; |
||||
23 | |||||
24 | use yii\db\Exception; |
||||
25 | use yii\web\HttpException; |
||||
26 | |||||
27 | /** @noinspection MissingPropertyAnnotationsInspection */ |
||||
0 ignored issues
–
show
|
|||||
28 | |||||
29 | /** |
||||
0 ignored issues
–
show
|
|||||
30 | * @author nystudio107 |
||||
0 ignored issues
–
show
Content of the @author tag must be in the form "Display Name <[email protected]>"
![]() |
|||||
31 | * @package Retour |
||||
0 ignored issues
–
show
|
|||||
32 | * @since 3.0.0 |
||||
0 ignored issues
–
show
|
|||||
33 | */ |
||||
0 ignored issues
–
show
|
|||||
34 | class Statistics extends Component |
||||
35 | { |
||||
36 | // Constants |
||||
37 | // ========================================================================= |
||||
38 | |||||
39 | const LAST_STATISTICS_TRIM_CACHE_KEY = 'retour-last-statistics-trim'; |
||||
40 | |||||
41 | // Protected Properties |
||||
42 | // ========================================================================= |
||||
43 | |||||
44 | /** |
||||
0 ignored issues
–
show
|
|||||
45 | * @var null|array |
||||
46 | */ |
||||
47 | protected $cachedStatistics; |
||||
48 | |||||
49 | // Public Methods |
||||
50 | // ========================================================================= |
||||
51 | |||||
52 | /** |
||||
0 ignored issues
–
show
|
|||||
53 | * @return array All of the statistics |
||||
54 | */ |
||||
55 | public function getAllStatistics(): array |
||||
56 | { |
||||
57 | // Cache it in our class; no need to fetch it more than once |
||||
58 | if ($this->cachedStatistics !== null) { |
||||
59 | return $this->cachedStatistics; |
||||
60 | } |
||||
61 | // Query the db table |
||||
62 | $stats = (new Query()) |
||||
63 | ->from(['{{%retour_stats}}']) |
||||
64 | ->orderBy('hitCount DESC') |
||||
65 | ->limit(Retour::$settings->statsDisplayLimit) |
||||
66 | ->all(); |
||||
67 | // Cache for future accesses |
||||
68 | $this->cachedStatistics = $stats; |
||||
69 | |||||
70 | return $stats; |
||||
71 | } |
||||
72 | |||||
73 | /** |
||||
0 ignored issues
–
show
|
|||||
74 | * @param int $days The number of days to get |
||||
0 ignored issues
–
show
|
|||||
75 | * @param bool $handled |
||||
0 ignored issues
–
show
|
|||||
76 | * |
||||
77 | * @return array Recent statistics |
||||
78 | */ |
||||
79 | public function getRecentStatistics($days = 1, $handled = false): array |
||||
80 | { |
||||
81 | // Ensure is an int |
||||
82 | $handledInt = (int)$handled; |
||||
83 | $stats = []; |
||||
84 | $db = Craft::$app->getDb(); |
||||
85 | if ($db->getIsMysql()) { |
||||
86 | // Query the db table |
||||
87 | $stats = (new Query()) |
||||
88 | ->from(['{{%retour_stats}}']) |
||||
89 | ->where("hitLastTime >= ( CURDATE() - INTERVAL '{$days}' DAY )") |
||||
90 | ->andWhere("handledByRetour = {$handledInt}") |
||||
91 | ->orderBy('hitLastTime DESC') |
||||
92 | ->all(); |
||||
93 | } |
||||
94 | if ($db->getIsPgsql()) { |
||||
95 | // Query the db table |
||||
96 | $stats = (new Query()) |
||||
97 | ->from(['{{%retour_stats}}']) |
||||
98 | ->where("\"hitLastTime\" >= ( CURRENT_TIMESTAMP - INTERVAL '{$days} days' )") |
||||
99 | ->andWhere(['handledByRetour' => $handledInt]) |
||||
100 | ->orderBy('hitLastTime DESC') |
||||
101 | ->all(); |
||||
102 | } |
||||
103 | |||||
104 | return $stats; |
||||
105 | } |
||||
106 | |||||
107 | /** |
||||
0 ignored issues
–
show
|
|||||
108 | * @return int |
||||
109 | */ |
||||
110 | public function clearStatistics(): int |
||||
111 | { |
||||
112 | $db = Craft::$app->getDb(); |
||||
113 | try { |
||||
114 | $result = $db->createCommand() |
||||
115 | ->truncateTable('{{%retour_stats}}') |
||||
116 | ->execute(); |
||||
117 | } catch (Exception $e) { |
||||
118 | $result = -1; |
||||
119 | Craft::error($e->getMessage(), __METHOD__); |
||||
120 | } |
||||
121 | |||||
122 | return $result; |
||||
123 | } |
||||
124 | |||||
125 | /** |
||||
126 | * Delete a statistic by id |
||||
127 | * |
||||
128 | * @param int $id |
||||
0 ignored issues
–
show
|
|||||
129 | * |
||||
130 | * @return int The result |
||||
131 | */ |
||||
132 | public function deleteStatisticById(int $id): int |
||||
133 | { |
||||
134 | $db = Craft::$app->getDb(); |
||||
135 | // Delete a row from the db table |
||||
136 | try { |
||||
137 | $result = $db->createCommand()->delete( |
||||
138 | '{{%retour_stats}}', |
||||
139 | [ |
||||
140 | 'id' => $id, |
||||
141 | ] |
||||
142 | )->execute(); |
||||
143 | } catch (Exception $e) { |
||||
144 | Craft::error($e->getMessage(), __METHOD__); |
||||
145 | $result = 0; |
||||
146 | } |
||||
147 | |||||
148 | return $result; |
||||
149 | } |
||||
150 | |||||
151 | /** |
||||
152 | * Increment the retour_stats record |
||||
153 | * |
||||
154 | * @param string $url The 404 url |
||||
0 ignored issues
–
show
|
|||||
155 | * @param bool $handled |
||||
0 ignored issues
–
show
|
|||||
156 | */ |
||||
0 ignored issues
–
show
|
|||||
157 | public function incrementStatistics(string $url, $handled = false) |
||||
158 | { |
||||
159 | $referrer = $remoteIp = null; |
||||
160 | $request = Craft::$app->getRequest(); |
||||
161 | $siteId = Craft::$app->getSites()->currentSite->id; |
||||
162 | if (!$request->isConsoleRequest) { |
||||
163 | $referrer = $request->getReferrer(); |
||||
164 | if (Retour::$settings->recordRemoteIp) { |
||||
165 | $remoteIp = $request->getUserIP(); |
||||
166 | } |
||||
167 | $userAgent = $request->getUserAgent(); |
||||
168 | if (Retour::$currentException !== null) { |
||||
169 | $exceptionMessage = Retour::$currentException->getMessage(); |
||||
170 | $exceptionFilePath = Retour::$currentException->getFile(); |
||||
171 | $exceptionFileLine = Retour::$currentException->getLine(); |
||||
172 | } |
||||
173 | } |
||||
174 | $referrer = $referrer ?? ''; |
||||
175 | $remoteIp = $remoteIp ?? ''; |
||||
176 | $userAgent = $userAgent ?? ''; |
||||
177 | $exceptionMessage = $exceptionMessage ?? ''; |
||||
178 | $exceptionFilePath = $exceptionFilePath ?? ''; |
||||
179 | $exceptionFileLine = $exceptionFileLine ?? 0; |
||||
180 | // Strip the query string if `stripQueryStringFromStats` is set |
||||
181 | if (Retour::$settings->stripQueryStringFromStats) { |
||||
182 | $url = UrlHelper::stripQueryString($url); |
||||
183 | $referrer = UrlHelper::stripQueryString($referrer); |
||||
0 ignored issues
–
show
It seems like
$referrer can also be of type null ; however, parameter $url of craft\helpers\UrlHelper::stripQueryString() does only seem to accept string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
184 | } |
||||
185 | // Normalize the $url via the validator |
||||
186 | $stats = new StatsModel([ |
||||
0 ignored issues
–
show
|
|||||
187 | 'redirectSrcUrl' => $url, |
||||
188 | ]); |
||||
0 ignored issues
–
show
For multi-line function calls, the closing parenthesis should be on a new line.
If a function call spawns multiple lines, the coding standard suggests to move the closing parenthesis to a new line: someFunctionCall(
$firstArgument,
$secondArgument,
$thirdArgument
); // Closing parenthesis on a new line.
![]() |
|||||
189 | $stats->validate(); |
||||
190 | // Find any existing retour_stats record |
||||
191 | $statsConfig = (new Query()) |
||||
192 | ->from(['{{%retour_stats}}']) |
||||
193 | ->where(['redirectSrcUrl' => $stats->redirectSrcUrl]) |
||||
194 | ->one(); |
||||
195 | // If no record is found, initialize some values |
||||
196 | if ($statsConfig === null) { |
||||
197 | $stats->id = 0; |
||||
198 | $stats->hitCount = 0; |
||||
199 | } else { |
||||
200 | $stats->id = $statsConfig['id']; |
||||
201 | $stats->hitCount = $statsConfig['hitCount']; |
||||
202 | } |
||||
203 | // Merge in the updated info |
||||
204 | $stats->siteId = $siteId; |
||||
205 | $stats->referrerUrl = $referrer; |
||||
206 | $stats->remoteIp = $remoteIp; |
||||
207 | $stats->userAgent = $userAgent; |
||||
208 | $stats->exceptionMessage = $exceptionMessage; |
||||
209 | $stats->exceptionFilePath = $exceptionFilePath; |
||||
210 | $stats->exceptionFileLine = (int)$exceptionFileLine; |
||||
211 | $stats->hitLastTime = Db::prepareDateForDb(new \DateTime()); |
||||
212 | $stats->handledByRetour = (int)$handled; |
||||
213 | $stats->hitCount++; |
||||
214 | $statsConfig = $stats->getAttributes(); |
||||
215 | // Record the updated statistics |
||||
216 | $this->saveStatistics($statsConfig); |
||||
217 | // After incrementing a statistic, trim the retour_stats db table |
||||
218 | if (Retour::$settings->automaticallyTrimStatistics && !$this->rateLimited()) { |
||||
219 | $this->trimStatistics(); |
||||
220 | } |
||||
221 | } |
||||
222 | |||||
223 | /** |
||||
224 | * Trim the retour_stats db table based on the statsStoredLimit config.php |
||||
225 | * setting |
||||
226 | * |
||||
227 | * @param int|null $limit |
||||
0 ignored issues
–
show
|
|||||
228 | * |
||||
229 | * @return int |
||||
230 | */ |
||||
231 | public function trimStatistics(int $limit = null): int |
||||
232 | { |
||||
233 | $affectedRows = 0; |
||||
234 | $db = Craft::$app->getDb(); |
||||
235 | $quotedTable = $db->quoteTableName('{{%retour_stats}}'); |
||||
236 | $limit = $limit ?? Retour::$settings->statsStoredLimit; |
||||
237 | |||||
238 | if ($limit !== null) { |
||||
0 ignored issues
–
show
|
|||||
239 | // https://stackoverflow.com/questions/578867/sql-query-delete-all-records-from-the-table-except-latest-n |
||||
240 | try { |
||||
241 | if ($db->getIsMysql()) { |
||||
242 | // Handle MySQL |
||||
243 | $affectedRows = $db->createCommand(/** @lang mysql */ |
||||
0 ignored issues
–
show
|
|||||
244 | " |
||||
245 | DELETE FROM {$quotedTable} |
||||
246 | WHERE id NOT IN ( |
||||
247 | SELECT id |
||||
248 | FROM ( |
||||
249 | SELECT id |
||||
250 | FROM {$quotedTable} |
||||
251 | ORDER BY hitLastTime DESC |
||||
252 | LIMIT {$limit} |
||||
253 | ) foo |
||||
254 | ) |
||||
255 | " |
||||
256 | )->execute(); |
||||
257 | } |
||||
258 | if ($db->getIsPgsql()) { |
||||
259 | // Handle Postgres |
||||
260 | $affectedRows = $db->createCommand(/** @lang mysql */ |
||||
0 ignored issues
–
show
|
|||||
261 | " |
||||
262 | DELETE FROM {$quotedTable} |
||||
263 | WHERE id NOT IN ( |
||||
264 | SELECT id |
||||
265 | FROM ( |
||||
266 | SELECT id |
||||
267 | FROM {$quotedTable} |
||||
268 | ORDER BY \"hitLastTime\" DESC |
||||
269 | LIMIT {$limit} |
||||
270 | ) foo |
||||
271 | ) |
||||
272 | " |
||||
273 | )->execute(); |
||||
274 | } |
||||
275 | } catch (Exception $e) { |
||||
276 | Craft::error($e->getMessage(), __METHOD__); |
||||
277 | } |
||||
278 | Craft::info( |
||||
279 | Craft::t( |
||||
280 | 'retour', |
||||
281 | 'Trimmed {rows} from retour_stats table', |
||||
282 | ['rows' => $affectedRows] |
||||
283 | ), |
||||
284 | __METHOD__ |
||||
285 | ); |
||||
286 | } |
||||
287 | |||||
288 | return $affectedRows; |
||||
289 | } |
||||
290 | |||||
291 | /** |
||||
0 ignored issues
–
show
|
|||||
292 | * @param array $statsConfig |
||||
0 ignored issues
–
show
|
|||||
293 | */ |
||||
0 ignored issues
–
show
|
|||||
294 | public function saveStatistics(array $statsConfig) |
||||
295 | { |
||||
296 | // Validate the model before saving it to the db |
||||
297 | $stats = new StatsModel($statsConfig); |
||||
298 | if ($stats->validate() === false) { |
||||
299 | Craft::error( |
||||
300 | Craft::t( |
||||
301 | 'retour', |
||||
302 | 'Error validating statistics {id}: {errors}', |
||||
303 | ['id' => $stats->id, 'errors' => print_r($stats->getErrors(), true)] |
||||
304 | ), |
||||
305 | __METHOD__ |
||||
306 | ); |
||||
307 | |||||
308 | return; |
||||
309 | } |
||||
310 | // Get the validated model attributes and save them to the db |
||||
311 | $statsConfig = $stats->getAttributes(); |
||||
312 | $db = Craft::$app->getDb(); |
||||
313 | if ($statsConfig['id'] !== 0) { |
||||
314 | // Update the existing record |
||||
315 | try { |
||||
316 | $result = $db->createCommand()->update( |
||||
0 ignored issues
–
show
|
|||||
317 | '{{%retour_stats}}', |
||||
318 | $statsConfig, |
||||
319 | [ |
||||
320 | 'id' => $statsConfig['id'], |
||||
321 | ] |
||||
322 | )->execute(); |
||||
323 | } catch (Exception $e) { |
||||
324 | // We don't log this error on purpose, because it's just a stats |
||||
325 | // update, and deadlock errors can potentially occur |
||||
326 | // Craft::error($e->getMessage(), __METHOD__); |
||||
327 | } |
||||
328 | } else { |
||||
329 | unset($statsConfig['id']); |
||||
330 | // Create a new record |
||||
331 | try { |
||||
332 | $db->createCommand()->insert( |
||||
333 | '{{%retour_stats}}', |
||||
334 | $statsConfig |
||||
335 | )->execute(); |
||||
336 | } catch (Exception $e) { |
||||
337 | Craft::error($e->getMessage(), __METHOD__); |
||||
338 | } |
||||
339 | } |
||||
340 | } |
||||
341 | |||||
342 | // Protected Methods |
||||
343 | // ========================================================================= |
||||
344 | |||||
345 | /** |
||||
346 | * Don't trim more than a given interval, so that performance is not affected |
||||
347 | * |
||||
348 | * @return bool |
||||
349 | */ |
||||
350 | protected function rateLimited(): bool |
||||
351 | { |
||||
352 | $limited = false; |
||||
353 | $now = round(microtime(true) * 1000); |
||||
354 | $cache = Craft::$app->getCache(); |
||||
355 | $then = $cache->get(self::LAST_STATISTICS_TRIM_CACHE_KEY); |
||||
356 | if (($then !== false) && ($now - (int)$then < Retour::$settings->statisticsRateLimitMs)) { |
||||
357 | $limited = true; |
||||
358 | } |
||||
359 | $cache->set(self::LAST_STATISTICS_TRIM_CACHE_KEY, $now, 0); |
||||
360 | |||||
361 | return $limited; |
||||
362 | } |
||||
363 | } |
||||
364 |