PluginProbe
404 Solution / 4.2.0
404 Solution v4.2.0
4.3.5 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 4.2.0 4.1.19 4.1.18 4.1.17 4.1.16 4.1.15 4.1.13 4.1.12 4.1.11 4.1.10 4.1.9 4.1.8 4.1.7 4.1.6 4.1.5 4.1.4 4.1.3 trunk 2.30.0 All 109 releases
404-solution / includes / ViewReadService.php

ViewReadService.php in 404 Solution 4.2.0, at includes/ViewReadService.php

1,109 lines 44.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!defined('ABSPATH')) {
4 exit;
5 }
6
7 require_once __DIR__ . '/ViewSnapshotCache.php';
8
9 /**
10 * Admin list view read path, snapshot caching, and status counts.
11 *
12 * Extracted from DataAccess in Phase 6 of the DataAccess refactor.
13 * Delegates to four collaborators: ViewQueryBuilder (SQL construction),
14 * ViewDiagnostics (failure diagnostics), ViewCacheInvalidator (cache
15 * clearing), and ViewSnapshotCache (cache CRUD and warmup).
16 *
17 * @see docs/dataaccess-refactor-plan.md Phase 6.
18 */
19 class ABJ_404_Solution_ViewReadService implements ABJ_404_Solution_ViewReadServiceInterface, ABJ_404_Solution_ViewSnapshotCacheHostInterface {
20 /** @var bool Legacy reflection bridge for tests and old diagnostics. */
21 private static $viewSnapshotTableEnsured = false;
22
23 // --- Constants ---
24
25 const CACHE_KEY_REDIRECT_STATUS = ABJ_404_Solution_ViewReadRuntimeState::CACHE_KEY_REDIRECT_STATUS;
26 const CACHE_KEY_CAPTURED_STATUS = ABJ_404_Solution_ViewReadRuntimeState::CACHE_KEY_CAPTURED_STATUS;
27 const CACHE_KEY_HIGH_IMPACT_CAPTURED = ABJ_404_Solution_ViewReadRuntimeState::CACHE_KEY_HIGH_IMPACT_CAPTURED;
28 const STATUS_CACHE_TTL = ABJ_404_Solution_ViewReadRuntimeState::STATUS_CACHE_TTL;
29 const STATUS_CACHE_TIMEOUT_SELFHEAL_TTL = ABJ_404_Solution_ViewReadRuntimeState::STATUS_CACHE_TIMEOUT_SELFHEAL_TTL;
30 const VIEW_SNAPSHOT_CACHE_TTL_SECONDS = ABJ_404_Solution_ViewReadRuntimeState::VIEW_SNAPSHOT_CACHE_TTL_SECONDS;
31 const VIEW_SNAPSHOT_REFRESH_COOLDOWN_SECONDS = ABJ_404_Solution_ViewReadRuntimeState::VIEW_SNAPSHOT_REFRESH_COOLDOWN_SECONDS;
32 const VIEW_SNAPSHOT_WARMUP_STAGE_TIMEOUT_SECONDS = ABJ_404_Solution_ViewReadRuntimeState::VIEW_SNAPSHOT_WARMUP_STAGE_TIMEOUT_SECONDS;
33 const VIEW_SNAPSHOT_WARMUP_STALE_SECONDS = ABJ_404_Solution_ViewReadRuntimeState::VIEW_SNAPSHOT_WARMUP_STALE_SECONDS;
34 const VIEW_SNAPSHOT_WARMUP_MAX_ATTEMPTS = ABJ_404_Solution_ViewReadRuntimeState::VIEW_SNAPSHOT_WARMUP_MAX_ATTEMPTS;
35 const VIEW_SNAPSHOT_MAX_PAYLOAD_BYTES = ABJ_404_Solution_ViewReadRuntimeState::VIEW_SNAPSHOT_MAX_PAYLOAD_BYTES;
36 const HITS_TABLE_LAST_CHECKED_FLAG = ABJ_404_Solution_ViewReadRuntimeState::HITS_TABLE_LAST_CHECKED_FLAG;
37 const HITS_TABLE_LAST_DECISION_FLAG = ABJ_404_Solution_ViewReadRuntimeState::HITS_TABLE_LAST_DECISION_FLAG;
38 const LOGS_COUNT_CACHE_TTL_SECONDS = ABJ_404_Solution_ViewReadRuntimeState::LOGS_COUNT_CACHE_TTL_SECONDS;
39
40 // --- Static properties ---
41
42 /**
43 * Per-request "bulk mutation in progress" flag.
44 *
45 * @var bool
46 */
47 public static $bulkMutationInProgress = false;
48
49 // --- Dependencies (constructor injection) ---
50
51 /** @var ABJ_404_Solution_DatabaseCore */
52 private $dbCore;
53
54 /** @var ABJ_404_Solution_LogsRepository */
55 private $logsRepo;
56
57 /** @var ABJ_404_Solution_Functions */
58 private $f;
59
60 /** @var ABJ_404_Solution_Logging */
61 private $logger;
62
63 // --- Collaborators ---
64
65 /** @var ABJ_404_Solution_ViewQueryBuilder */
66 private $queryBuilder;
67
68 /** @var ABJ_404_Solution_ViewDiagnostics */
69 private $diagnostics;
70
71 /** @var ABJ_404_Solution_ViewCacheInvalidator */
72 private $cacheInvalidator;
73
74 /** @var ABJ_404_Solution_ViewSnapshotCache */
75 private $snapshotCache;
76
77 // --- ViewBuildOrchestrator bridge ---
78
79 /** @var ABJ_404_Solution_ViewBuildOrchestratorInterface|null */
80 private $viewBuildOrchestrator;
81
82 // --- Instance property ---
83
84 /** @var array<string, int> */
85 private $redirectsForViewCountRequestCache = array();
86
87 /**
88 * @param ABJ_404_Solution_DatabaseCore $dbCore
89 * @param ABJ_404_Solution_LogsRepository $logsRepo
90 * @param ABJ_404_Solution_RedirectsRepository $redirectsRepo
91 * @param ABJ_404_Solution_Functions|null $f Falls back to abj_service('functions')
92 * @param ABJ_404_Solution_Logging|null $logger Falls back to abj_service('logging')
93 */
94 public function __construct(
95 ABJ_404_Solution_DatabaseCore $dbCore,
96 ABJ_404_Solution_LogsRepository $logsRepo,
97 ABJ_404_Solution_RedirectsRepository $redirectsRepo,
98 $f = null,
99 $logger = null
100 ) {
101 $this->dbCore = $dbCore;
102 $this->logsRepo = $logsRepo;
103 $this->f = $f !== null ? $f : abj_service('functions');
104 $this->logger = $logger !== null ? $logger : abj_service('logging');
105
106 $this->diagnostics = new ABJ_404_Solution_ViewDiagnostics($dbCore);
107 $this->cacheInvalidator = new ABJ_404_Solution_ViewCacheInvalidator(
108 $dbCore, $redirectsRepo, $this->viewDoneFreshnessOptionName()
109 );
110 $this->queryBuilder = new ABJ_404_Solution_ViewQueryBuilder(
111 $dbCore, $this->f, $logsRepo, $this->logger
112 );
113 $this->queryBuilder->setHost($this);
114 $this->snapshotCache = new ABJ_404_Solution_ViewSnapshotCache($dbCore, $this->logger);
115 $this->snapshotCache->setHost($this);
116 }
117
118 /**
119 * @param ABJ_404_Solution_ViewBuildOrchestratorInterface $viewBuildOrchestrator
120 * @return void
121 */
122 public function setViewBuildOrchestrator(ABJ_404_Solution_ViewBuildOrchestratorInterface $viewBuildOrchestrator): void {
123 $this->viewBuildOrchestrator = $viewBuildOrchestrator;
124 $this->cacheInvalidator->setViewBuildOrchestrator($viewBuildOrchestrator);
125 $this->queryBuilder->setViewBuildOrchestrator($viewBuildOrchestrator);
126 $this->snapshotCache->setViewBuildOrchestrator($viewBuildOrchestrator);
127 }
128
129 /** @return ABJ_404_Solution_ViewBuildOrchestratorInterface */
130 private function requireViewBuildOrchestrator(): ABJ_404_Solution_ViewBuildOrchestratorInterface {
131 if ($this->viewBuildOrchestrator === null) {
132 throw new \RuntimeException('ViewReadService requires ViewBuildOrchestrator (call setViewBuildOrchestrator first)'); // allow-raw-error: assertion, should never reach user
133 }
134 return $this->viewBuildOrchestrator;
135 }
136
137 /** @param bool $value @return void */
138 public static function setViewSnapshotTableEnsured(bool $value): void {
139 self::$viewSnapshotTableEnsured = $value;
140 ABJ_404_Solution_ViewSnapshotCache::setViewSnapshotTableEnsured($value);
141 }
142
143 /** @return bool */
144 public static function isViewSnapshotTableEnsured(): bool {
145 return self::$viewSnapshotTableEnsured;
146 }
147
148 /** @return string */
149 private function viewDoneFreshnessOptionName(): string {
150 return $this->dbCore->getLowercasePrefix() . 'abj404_view_done_built_at';
151 }
152
153 /**
154 * @param mixed $value
155 * @return int
156 */
157 private static function scalarToInt($value): int {
158 return is_scalar($value) ? intval($value) : 0;
159 }
160
161 // =========================================================================
162 // Interface-satisfying read methods and lightweight accessors
163 // =========================================================================
164
165 /**
166 * @param bool $bypassCache
167 * @return array<string, int>
168 */
169 function getRedirectStatusCounts($bypassCache = false): array {
170 if (!$bypassCache) {
171 $cached = get_transient(self::CACHE_KEY_REDIRECT_STATUS);
172 if ($cached !== false && is_array($cached)) {
173 /** @var array<string, int> $cached */
174 return $cached;
175 }
176 }
177
178 $query = "SELECT
179 SUM(CASE WHEN disabled = 0 THEN 1 ELSE 0 END) as active_count,
180 SUM(CASE WHEN disabled = 0 AND status = " . ABJ404_STATUS_MANUAL . " THEN 1 ELSE 0 END) as manual_count,
181 SUM(CASE WHEN disabled = 0 AND status = " . ABJ404_STATUS_AUTO . " THEN 1 ELSE 0 END) as auto_count,
182 SUM(CASE WHEN disabled = 0 AND status = " . ABJ404_STATUS_REGEX . " THEN 1 ELSE 0 END) as regex_count,
183 SUM(CASE WHEN disabled = 1 THEN 1 ELSE 0 END) as trash_count
184 FROM {wp_abj404_redirects}
185 WHERE status IN (" . ABJ404_STATUS_MANUAL . ", " . ABJ404_STATUS_AUTO . ", " . ABJ404_STATUS_REGEX . ")";
186 $query = $this->dbCore->doTableNameReplacements($query);
187
188 $result = $this->dbCore->queryAndGetResults($query);
189 $hadError = !empty($result['last_error']) || !empty($result['timed_out']);
190 $rows = is_array($result['rows']) ? $result['rows'] : array();
191
192 $counts = array('all' => 0, 'manual' => 0, 'auto' => 0, 'regex' => 0, 'trash' => 0);
193 if (!empty($rows)) {
194 $row = is_array($rows[0] ?? null) ? $rows[0] : array();
195 $activeCount = $row['active_count'] ?? 0;
196 $manualCount = $row['manual_count'] ?? 0;
197 $autoCount = $row['auto_count'] ?? 0;
198 $regexCount = $row['regex_count'] ?? 0;
199 $trashCount = $row['trash_count'] ?? 0;
200 $counts = array(
201 'all' => self::scalarToInt($activeCount),
202 'manual' => self::scalarToInt($manualCount),
203 'auto' => self::scalarToInt($autoCount),
204 'regex' => self::scalarToInt($regexCount),
205 'trash' => self::scalarToInt($trashCount)
206 );
207 }
208
209 if (!$hadError && !$bypassCache) {
210 set_transient(self::CACHE_KEY_REDIRECT_STATUS, $counts, self::STATUS_CACHE_TTL);
211 }
212
213 return $counts;
214 }
215
216 /**
217 * @param bool $bypassCache
218 * @return array<string, int>
219 */
220 function getCapturedStatusCounts($bypassCache = false): array {
221 if (!$bypassCache) {
222 $cached = get_transient(self::CACHE_KEY_CAPTURED_STATUS);
223 if ($cached !== false && is_array($cached)) {
224 /** @var array<string, int> $cached */
225 return $cached;
226 }
227 }
228
229 $query = "SELECT
230 COUNT(*) as total,
231 SUM(CASE WHEN disabled = 0 THEN 1 ELSE 0 END) as active,
232 SUM(CASE WHEN disabled = 0 AND status = " . ABJ404_STATUS_CAPTURED . " THEN 1 ELSE 0 END) as captured,
233 SUM(CASE WHEN disabled = 0 AND status = " . ABJ404_STATUS_IGNORED . " THEN 1 ELSE 0 END) as ignored,
234 SUM(CASE WHEN disabled = 0 AND status = " . ABJ404_STATUS_LATER . " THEN 1 ELSE 0 END) as later,
235 SUM(CASE WHEN disabled = 1 THEN 1 ELSE 0 END) as trash
236 FROM {wp_abj404_redirects}
237 WHERE status IN (" . ABJ404_STATUS_CAPTURED . ", " . ABJ404_STATUS_IGNORED . ", " . ABJ404_STATUS_LATER . ")";
238 $query = $this->dbCore->doTableNameReplacements($query);
239
240 $result = $this->dbCore->queryAndGetResults($query);
241 $hadError = !empty($result['last_error']) || !empty($result['timed_out']);
242 $rows = is_array($result['rows']) ? $result['rows'] : array();
243
244 $counts = array('all' => 0, 'captured' => 0, 'ignored' => 0, 'later' => 0, 'trash' => 0);
245 if (!empty($rows)) {
246 $row = is_array($rows[0] ?? null) ? $rows[0] : array();
247 $activeCount = $row['active'] ?? 0;
248 $capturedCount = $row['captured'] ?? 0;
249 $ignoredCount = $row['ignored'] ?? 0;
250 $laterCount = $row['later'] ?? 0;
251 $trashCount = $row['trash'] ?? 0;
252 $counts = array(
253 'all' => self::scalarToInt($activeCount),
254 'captured' => self::scalarToInt($capturedCount),
255 'ignored' => self::scalarToInt($ignoredCount),
256 'later' => self::scalarToInt($laterCount),
257 'trash' => self::scalarToInt($trashCount)
258 );
259 }
260
261 if (!$hadError && !$bypassCache) {
262 set_transient(self::CACHE_KEY_CAPTURED_STATUS, $counts, self::STATUS_CACHE_TTL);
263 }
264
265 return $counts;
266 }
267
268 /**
269 * @return int
270 */
271 function getHighImpactCapturedCount(): int {
272 $cached = get_transient(self::CACHE_KEY_HIGH_IMPACT_CAPTURED);
273 if ($cached !== false) {
274 return intval(is_scalar($cached) ? $cached : 0);
275 }
276
277 if (!$this->logsRepo->logsHitsTableExists()) {
278 $this->logsRepo->scheduleHitsTableRebuild();
279 return 0;
280 }
281
282 $query = $this->queryBuilder->buildHighImpactCapturedCountQuery();
283
284 $result = $this->queryWithTimeout($query, 60);
285 $timedOut = !empty($result['timed_out']);
286 $hadError = !empty($result['last_error']) || $timedOut;
287 $rows = is_array($result['rows']) ? $result['rows'] : array();
288 $firstRow = (!empty($rows) && is_array($rows[0] ?? null)) ? $rows[0] : array();
289 $count = self::scalarToInt($firstRow['cnt'] ?? 0);
290
291 if ($timedOut) {
292 $this->logsRepo->scheduleHitsTableRebuild();
293 // allow-cache-empty: timeout self-heal sentinel, 5-minute window. Real value returns once the rebuild completes and the short cache expires.
294 set_transient(self::CACHE_KEY_HIGH_IMPACT_CAPTURED, 0, self::STATUS_CACHE_TIMEOUT_SELFHEAL_TTL);
295 return 0;
296 }
297
298 if ($hadError) {
299 return 0;
300 }
301
302 if ($count === 0) {
303 if ($this->isHitsTableEmpty()) {
304 $this->logsRepo->scheduleHitsTableRebuild();
305 return 0;
306 }
307 }
308
309 set_transient(self::CACHE_KEY_HIGH_IMPACT_CAPTURED, $count, self::STATUS_CACHE_TTL);
310
311 return $count;
312 }
313
314 /**
315 * @return bool
316 */
317 private function isHitsTableEmpty(): bool {
318 $check = "SELECT 1 FROM {wp_abj404_logs_hits} LIMIT 1";
319 $check = $this->dbCore->doTableNameReplacements($check);
320 $result = $this->dbCore->queryAndGetResults($check);
321 if (!empty($result['last_error']) || !empty($result['timed_out'])) {
322 return false;
323 }
324 $rows = is_array($result['rows'] ?? null) ? $result['rows'] : array();
325 return empty($rows);
326 }
327
328 /**
329 * @param string $query
330 * @param int $timeoutSeconds
331 * @return array<string, mixed>
332 */
333 private function queryWithTimeout(string $query, int $timeoutSeconds = 60): array {
334 return $this->dbCore->queryAndGetResults($query, array(
335 'timeout' => $timeoutSeconds,
336 ));
337 }
338
339 /** @return string|null */
340 private function logsCountCacheKey(int $logID): ?string {
341 if ($logID !== 0 || !function_exists('get_transient')) {
342 return null;
343 }
344
345 return 'abj404_logs_count_v1_' . $this->currentBlogIdForCache() . '_' . $this->maxLogIdForCache();
346 }
347
348 /** @return int */
349 private function currentBlogIdForCache(): int {
350 if (!function_exists('get_current_blog_id')) {
351 return 1;
352 }
353
354 $rawBlogId = function_exists('absint')
355 ? absint(get_current_blog_id())
356 : abs(intval(get_current_blog_id()));
357
358 return $rawBlogId > 0 ? $rawBlogId : 1;
359 }
360
361 /** @return int */
362 private function maxLogIdForCache(): int {
363 try {
364 return max(0, intval($this->logsRepo->getMaxLogId()));
365 } catch (Throwable $e) {
366 $this->logger->debugMessage(__FUNCTION__ . ' getMaxLogId() failed: '
367 . $e->getMessage() . '. Falling back to maxLogId=0.');
368 return 0;
369 }
370 }
371
372 /**
373 * @param int $logID
374 * @return int
375 */
376 function getLogsCount($logID) {
377 $logID = absint($logID);
378
379 $cacheKey = $this->logsCountCacheKey($logID);
380 if ($cacheKey !== null) {
381 $cached = get_transient($cacheKey);
382 if (is_numeric($cached)) {
383 return (int)$cached;
384 }
385 }
386
387 $query = ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/sql/getLogsCount.sql");
388
389 if ($logID != 0) {
390 $query = $this->f->str_replace('/* {SPECIFIC_ID}', '', $query);
391 $query = $this->f->str_replace('{logID}', (string)$logID, $query);
392 }
393
394 $result = $this->dbCore->queryAndGetResults($query);
395 $hadError = !empty($result['timed_out'])
396 || (isset($result['last_error']) && $result['last_error'] != '');
397
398 $rows = is_array($result['rows'] ?? null) ? $result['rows'] : array();
399 $count = 0;
400 if (!empty($rows)) {
401 $first = $rows[0];
402 $value = is_array($first) ? reset($first) : $first;
403 $count = self::scalarToInt($value);
404 }
405
406 if (!$hadError && $cacheKey !== null && function_exists('set_transient')
407 && empty($GLOBALS['abj404_feedback_preview_readonly'])) {
408 set_transient($cacheKey, $count, self::LOGS_COUNT_CACHE_TTL_SECONDS);
409 }
410
411 return function_exists('apply_filters')
412 ? (int) apply_filters('abj404_logs_count', $count, $logID)
413 : $count;
414 }
415
416 /** @return array<int, array<string, mixed>> */
417 function getRedirectsAll() {
418 $query = "select id, url from {wp_abj404_redirects} order by url";
419
420 $result = $this->dbCore->queryAndGetResults($query);
421 if (!empty($result['timed_out']) || (isset($result['last_error']) && $result['last_error'] != '')) {
422 return array();
423 }
424 $rows = is_array($result['rows'] ?? null) ? $result['rows'] : array();
425 return $rows;
426 }
427
428 /** @param string $tempFile @return void */
429 function doRedirectsExport(string $tempFile): void {
430 global $wpdb;
431
432 if (file_exists($tempFile)) {
433 ABJ_404_Solution_Functions::safeUnlink($tempFile);
434 }
435
436 $query = ABJ_404_Solution_Functions::readFileContents(__DIR__ .
437 "/sql/getRedirectsExport.sql");
438 $query = $this->dbCore->doTableNameReplacements($query);
439
440 $result = mysqli_query($wpdb->dbh, $query);
441 if ($result instanceof \mysqli_result) {
442 $fh = fopen($tempFile, 'w');
443 if ($fh === false) {
444 return;
445 }
446 fputcsv($fh, array('from_url', 'status', 'type', 'to_url', 'wp_type', 'engine', 'code'), ',', '"', '\\');
447
448 while (($row = mysqli_fetch_array($result, MYSQLI_ASSOC))) {
449 fputcsv($fh, array(
450 $row['from_url'],
451 $row['status'],
452 $row['type'],
453 $row['to_url'],
454 $row['type_wp'],
455 isset($row['engine']) ? $row['engine'] : '',
456 isset($row['code']) ? $row['code'] : '301'
457 ), ',', '"', '\\');
458 }
459 fclose($fh);
460 mysqli_free_result($result);
461 }
462 }
463
464 /** @return array<int, array<string, mixed>> */
465 function getRedirectsWithLogs() {
466 $query = ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/sql/getRedirectsWithLogs.sql");
467
468 $result = $this->dbCore->queryAndGetResults($query);
469 if (!empty($result['timed_out']) || (isset($result['last_error']) && $result['last_error'] != '')) {
470 return array();
471 }
472 $rows = is_array($result['rows'] ?? null) ? $result['rows'] : array();
473 return $rows;
474 }
475
476 /** @return array<int, array<string, mixed>> */
477 function getRedirectsWithRegEx() {
478 $cached = ABJ_404_Solution_RedirectsRepository::getRegexRedirectsCache();
479 $disabled = ABJ_404_Solution_RedirectsRepository::isRegexCacheDisabled();
480
481 if ($cached !== null && !$disabled) {
482 return $cached;
483 }
484
485 if ($disabled) {
486 return $this->queryBuilder->queryRegexRedirects();
487 }
488
489 $results = $this->queryBuilder->queryRegexRedirects();
490
491 if (count($results) <= ABJ_404_Solution_RedirectsRepository::REGEX_CACHE_MAX_COUNT) {
492 ABJ_404_Solution_RedirectsRepository::setRegexRedirectsCache($results);
493 } else {
494 ABJ_404_Solution_RedirectsRepository::setRegexCacheDisabled(true);
495 }
496
497 return $results;
498 }
499
500 /** @return array<int, array<string, mixed>> */
501 function getManualRedirectsWithRegexMetachars() {
502 $query = "select \n {wp_abj404_redirects}.id,\n {wp_abj404_redirects}.url,\n {wp_abj404_redirects}.status,\n"
503 . " {wp_abj404_redirects}.type,\n {wp_abj404_redirects}.final_dest,\n {wp_abj404_redirects}.code,\n"
504 . " {wp_abj404_redirects}.timestamp,\n {wp_posts}.id as wp_post_id\n ";
505 $query .= "from {wp_abj404_redirects}\n " .
506 " LEFT OUTER JOIN {wp_posts} \n " .
507 " on {wp_abj404_redirects}.final_dest = {wp_posts}.id \n ";
508
509 $query .= "where status = " . ABJ404_STATUS_MANUAL . " \n " .
510 " and disabled = 0 \n " .
511 " and (INSTR(`url`, '*') > 0 " .
512 " OR INSTR(`url`, '[') > 0 " .
513 " OR INSTR(`url`, ']') > 0 " .
514 " OR INSTR(`url`, '|') > 0 " .
515 " OR INSTR(`url`, '^') > 0 " .
516 " OR INSTR(`url`, '\\\\') > 0 " .
517 " OR INSTR(`url`, '{') > 0 " .
518 " OR INSTR(`url`, '}') > 0)";
519 $results = $this->dbCore->queryAndGetResults($query);
520
521 /** @var array<int, array<string, mixed>> $rows */
522 $rows = is_array($results['rows']) ? $results['rows'] : array();
523 return $rows;
524 }
525
526 /**
527 * @param string $sub
528 * @param array<string, mixed> $tableOptions
529 * @return array<int|string, mixed>
530 */
531 function getRedirectsForView($sub, $tableOptions) {
532 $canUseSnapshotCache = $this->snapshotCache->canUseViewTableSnapshotCache($tableOptions);
533 $queryTimeout = isset($tableOptions['_abj404_query_timeout']) && is_numeric($tableOptions['_abj404_query_timeout'])
534 ? max(1, intval($tableOptions['_abj404_query_timeout'])) : 0;
535 $throwOnQueryError = !empty($tableOptions['_abj404_throw_on_view_query_error']);
536 $snapshotCacheKey = '';
537 if ($canUseSnapshotCache && $queryTimeout <= 0) {
538 $snapshotCacheKey = $this->snapshotCache->getViewSnapshotCacheKey('abj404_view_rows', $sub, $tableOptions);
539 $cachedRowsFromTable = $this->snapshotCache->getViewRowsSnapshotFromTable($snapshotCacheKey, false, false);
540 if (is_array($cachedRowsFromTable)) {
541 return $cachedRowsFromTable;
542 }
543 if (function_exists('get_transient')) {
544 $cachedRows = get_transient($snapshotCacheKey);
545 if (is_array($cachedRows)) {
546 return $cachedRows;
547 }
548 }
549 }
550
551 try {
552 $rows = $this->requireViewBuildOrchestrator()->runRedirectsForViewStaged((string)$sub, is_array($tableOptions) ? $tableOptions : array());
553 } catch (ABJ_404_Solution_ViewBuildPendingException $pending) {
554 if ($throwOnQueryError) {
555 throw $pending;
556 }
557 $this->logger->debugMessage('[staged] getRedirectsForView pending: ' . $pending->getMessage());
558 return array();
559 } catch (Throwable $e) {
560 if ($throwOnQueryError) {
561 $stagedFailureMarker = '/* staged: ' . $e->getMessage() . ' */';
562 $diagnostics = $this->diagnostics->captureViewQueryFailureDiagnostics(
563 (string)$sub,
564 $stagedFailureMarker,
565 is_array($tableOptions) ? $tableOptions : array(),
566 array('last_error' => $e->getMessage(), 'timed_out' => false)
567 );
568 $diagnostics['failed_query_label'] = 'getRedirectsForView';
569 $diagnostics['staged_error'] = $e->getMessage();
570 $message = 'getRedirectsForView failed; last_error=' . $e->getMessage()
571 . '; timed_out=false; sql_source=' . $stagedFailureMarker;
572 throw new ABJ_404_Solution_ViewQueryFailureException($message, $diagnostics);
573 }
574 $this->logger->errorMessage('[staged] getRedirectsForView failed: ' . $e->getMessage(),
575 $e instanceof \Exception ? $e : null);
576 return array();
577 }
578
579 $this->logger->debugMessage(sprintf(
580 '[staged] getRedirectsForView returned %d rows for page %s',
581 count($rows),
582 (string)$sub
583 ));
584
585 if ($canUseSnapshotCache && $snapshotCacheKey === '') {
586 $snapshotCacheKey = $this->snapshotCache->getViewSnapshotCacheKey('abj404_view_rows', $sub, $tableOptions);
587 }
588 if ($canUseSnapshotCache && $snapshotCacheKey !== '') {
589 $this->snapshotCache->setViewRowsSnapshotToTable($snapshotCacheKey, $sub, $rows, self::VIEW_SNAPSHOT_CACHE_TTL_SECONDS);
590 if (function_exists('set_transient')) {
591 // allow-cache-empty: empty $rows is a legitimate result on a fresh install (no redirects yet); error paths early-return above without reaching this line
592 set_transient($snapshotCacheKey, $rows, self::VIEW_SNAPSHOT_CACHE_TTL_SECONDS);
593 }
594 }
595
596 return $rows;
597 }
598
599 /**
600 * @param string $sub
601 * @param array<string, mixed> $tableOptions
602 * @return bool
603 */
604 function viewRowsSnapshotAvailable($sub, array $tableOptions): bool {
605 $canUseSnapshotCache = $this->snapshotCache->canUseViewTableSnapshotCache($tableOptions);
606 if (!$canUseSnapshotCache) {
607 return false;
608 }
609
610 $snapshotCacheKey = $this->snapshotCache->getViewSnapshotCacheKey('abj404_view_rows', $sub, $tableOptions);
611 $freshRows = $this->snapshotCache->getViewRowsSnapshotFromTable($snapshotCacheKey, false, false);
612 if (is_array($freshRows)) {
613 return true;
614 }
615 $recentRows = $this->snapshotCache->getViewRowsSnapshotFromTable($snapshotCacheKey, true, true);
616 if (is_array($recentRows)) {
617 return true;
618 }
619 if (function_exists('get_transient')) {
620 $transientRows = get_transient($snapshotCacheKey);
621 if (is_array($transientRows)) {
622 return true;
623 }
624 }
625
626 return false;
627 }
628
629 /**
630 * @param string $sub
631 * @param array<string, mixed> $tableOptions
632 * @return bool
633 */
634 function viewTableSnapshotAvailable($sub, array $tableOptions): bool {
635 if (!$this->viewRowsSnapshotAvailable($sub, $tableOptions)) {
636 return false;
637 }
638
639 $canUseSnapshotCache = function_exists('get_transient')
640 && $this->snapshotCache->canUseViewTableSnapshotCache($tableOptions);
641 if (!$canUseSnapshotCache) {
642 return false;
643 }
644
645 $countCacheKey = $this->snapshotCache->getViewSnapshotCacheKey('abj404_view_count', $sub, $tableOptions);
646 return get_transient($countCacheKey) !== false;
647 }
648
649 /**
650 * @param string $sub
651 * @param array<string, mixed> $tableOptions
652 * @return int
653 */
654 function getRedirectsForViewCount(string $sub, array $tableOptions): int {
655 $queryTimeout = isset($tableOptions['_abj404_query_timeout']) && is_numeric($tableOptions['_abj404_query_timeout'])
656 ? max(1, intval($tableOptions['_abj404_query_timeout'])) : 0;
657 $throwOnQueryError = !empty($tableOptions['_abj404_throw_on_view_query_error']);
658 $canUseSnapshotCache = function_exists('get_transient')
659 && $this->snapshotCache->canUseViewTableSnapshotCache($tableOptions);
660 $requestCountCacheKey = (string)$sub . '|' . md5(serialize($tableOptions));
661 $countCacheKey = '';
662 if ($canUseSnapshotCache && $queryTimeout <= 0) {
663 $countCacheKey = $this->snapshotCache->getViewSnapshotCacheKey('abj404_view_count', $sub, $tableOptions);
664 $cachedCount = get_transient($countCacheKey);
665 if ($cachedCount !== false) {
666 return intval(is_scalar($cachedCount) ? $cachedCount : 0);
667 }
668 }
669 if (array_key_exists($requestCountCacheKey, $this->redirectsForViewCountRequestCache)) {
670 return intval($this->redirectsForViewCountRequestCache[$requestCountCacheKey]);
671 }
672
673 $rawFilterText = is_string($tableOptions['filterText'] ?? null) ? $tableOptions['filterText'] : '';
674 if ($rawFilterText === '') {
675 $query = $this->queryBuilder->getOptimizedRedirectsForViewCountQuery($sub, $tableOptions);
676 $this->cacheInvalidator->setSqlBigSelects();
677 $queryOptions = $queryTimeout > 0 ? array('timeout' => $queryTimeout) : array();
678 $results = $this->dbCore->queryAndGetResults($query, $queryOptions);
679 $lastErrorRaw = $results['last_error'] ?? '';
680 $lastError = is_string($lastErrorRaw) ? $lastErrorRaw : '';
681 } else {
682 try {
683 $countValue = $this->requireViewBuildOrchestrator()->runRedirectsForViewCountStaged((string)$sub, $tableOptions);
684 $this->redirectsForViewCountRequestCache[$requestCountCacheKey] = $countValue;
685 if ($canUseSnapshotCache && $countCacheKey === '') {
686 $countCacheKey = $this->snapshotCache->getViewSnapshotCacheKey('abj404_view_count', $sub, $tableOptions);
687 }
688 if ($canUseSnapshotCache && $countCacheKey !== '') {
689 // allow-cache-empty: $countValue=0 is a legitimate result when no rows match the search filter; the staged pending/error paths throw above without reaching this line
690 set_transient($countCacheKey, $countValue, self::VIEW_SNAPSHOT_CACHE_TTL_SECONDS);
691 }
692 return $countValue;
693 } catch (ABJ_404_Solution_ViewBuildPendingException $pending) {
694 if ($throwOnQueryError) {
695 throw $pending;
696 }
697 $this->logger->debugMessage('[staged] getRedirectsForViewCount pending: ' . $pending->getMessage());
698 $this->redirectsForViewCountRequestCache[$requestCountCacheKey] = -1;
699 return -1;
700 } catch (Throwable $e) {
701 if ($throwOnQueryError) {
702 $stagedFailureMarker = '/* staged-count: ' . $e->getMessage() . ' */';
703 $diagnostics = $this->diagnostics->captureViewQueryFailureDiagnostics(
704 (string)$sub,
705 $stagedFailureMarker,
706 $tableOptions,
707 array('last_error' => $e->getMessage(), 'timed_out' => false)
708 );
709 $diagnostics['failed_query_label'] = 'getRedirectsForViewCount';
710 $diagnostics['staged_error'] = $e->getMessage();
711 throw new ABJ_404_Solution_ViewQueryFailureException($e->getMessage(), $diagnostics);
712 }
713 $this->logger->errorMessage('[staged] getRedirectsForViewCount failed: ' . $e->getMessage(),
714 $e instanceof \Exception ? $e : null);
715 $this->redirectsForViewCountRequestCache[$requestCountCacheKey] = -1;
716 return -1;
717 }
718 }
719
720 if ($throwOnQueryError && (!empty($results['timed_out']) || $lastError !== '')) {
721 $message = $this->diagnostics->formatViewQueryFailureMessage('getRedirectsForViewCount', $query, $results);
722 $diagnostics = $this->diagnostics->captureViewQueryFailureDiagnostics($sub, $query, $tableOptions, $results);
723 $diagnostics['failed_query_label'] = 'getRedirectsForViewCount';
724 throw new ABJ_404_Solution_ViewQueryFailureException($message, $diagnostics);
725 }
726
727 if ($lastError != '' && trim($lastError) != '') {
728 $diagnostics = $this->diagnostics->captureViewQueryFailureDiagnostics($sub, $query, $tableOptions, $results);
729 $diagnostics['failed_query_label'] = 'getRedirectsForViewCount';
730 throw new ABJ_404_Solution_ViewQueryFailureException(
731 "Error getting redirect count: " . esc_html($lastError),
732 $diagnostics
733 );
734 }
735 $rows = is_array($results['rows']) ? $results['rows'] : array();
736 if (empty($rows)) {
737 $this->redirectsForViewCountRequestCache[$requestCountCacheKey] = -1;
738 return -1;
739 }
740 $row = is_array($rows[0] ?? null) ? $rows[0] : array();
741 $rawCount = $row['count'] ?? $row['COUNT(*)'] ?? reset($row);
742 $countValue = intval(is_scalar($rawCount) ? $rawCount : 0);
743 $this->redirectsForViewCountRequestCache[$requestCountCacheKey] = $countValue;
744 if ($canUseSnapshotCache && $countCacheKey === '') {
745 $countCacheKey = $this->snapshotCache->getViewSnapshotCacheKey('abj404_view_count', $sub, $tableOptions);
746 }
747 if ($canUseSnapshotCache && $countCacheKey !== '') {
748 set_transient($countCacheKey, $countValue, self::VIEW_SNAPSHOT_CACHE_TTL_SECONDS);
749 }
750 return $countValue;
751 }
752
753 /** @param array<int, string> $postIDs @return array<int, mixed> */
754 function getExtraDataToPermalinkSuggestions(array $postIDs): array {
755 $postIDs = array_map('absint', $postIDs);
756 $postIDJoined = implode(", ", $postIDs);
757
758 $query = ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/sql/getAdditionalPostData.sql");
759 $query = $this->f->str_replace('{IDS_TO_INCLUDE}', $postIDJoined, $query);
760 $query = $this->dbCore->doTableNameReplacements($query);
761 $query = $this->f->doNormalReplacements($query);
762
763 $results = $this->dbCore->queryAndGetResults($query);
764
765 /** @var array<int, mixed> $rows */
766 $rows = is_array($results['rows']) ? $results['rows'] : array();
767 return $rows;
768 }
769
770 /**
771 * @param string $query
772 * @param array<string, mixed> $data
773 * @return string
774 */
775 function prepare_query_wp($query, $data) {
776 global $wpdb;
777 list($prepared_query, $ordered_values) = $this->prepare_query($query, $data);
778 // DAO-bypass-approved: $wpdb->prepare is read-only string formatting; callers execute the result through queryAndGetResults
779 return $wpdb->prepare($prepared_query, $ordered_values);
780 }
781
782 /**
783 * @param string $query
784 * @param array<string, mixed> $data
785 * @return array{0: string, 1: array<int, mixed>}
786 */
787 function prepare_query($query, $data) {
788 $ordered_values = [];
789 $prepared_query = preg_replace_callback('/\{(\w+)\}/', function($matches) use ($data, &$ordered_values) {
790 $key = $matches[1];
791 if (!isset($data[$key])) {
792 return $matches[0];
793 }
794 $value = $data[$key];
795
796 $ordered_values[] = $value;
797
798 $placeholder_type = is_int($value) ? '%d' : '%s';
799
800 return $placeholder_type;
801 }, $query);
802
803 return [$prepared_query !== null ? $prepared_query : $query, $ordered_values];
804 }
805
806 // =========================================================================
807 // Delegated: ViewQueryBuilder
808 // =========================================================================
809
810 /** @return string */
811 function buildHighImpactCapturedCountQuery(): string {
812 return $this->queryBuilder->buildHighImpactCapturedCountQuery();
813 }
814
815 /**
816 * @param string $sub
817 * @param array<string, mixed> $tableOptions
818 * @param bool $queryAllRowsAtOnce
819 * @param int $limitStart
820 * @param int $limitEnd
821 * @param bool $selectCountOnly
822 * @return string
823 */
824 function getRedirectsForViewQuery($sub, $tableOptions, $queryAllRowsAtOnce,
825 $limitStart, $limitEnd, $selectCountOnly) {
826 return $this->queryBuilder->getRedirectsForViewQuery($sub, $tableOptions, $queryAllRowsAtOnce,
827 $limitStart, $limitEnd, $selectCountOnly);
828 }
829
830 /**
831 * @param string $sub
832 * @param array<string, mixed> $tableOptions
833 * @return array<int, array<string, mixed>>
834 */
835 public function readFromViewDone(string $sub, array $tableOptions): array {
836 return $this->queryBuilder->readFromViewDone($sub, $tableOptions);
837 }
838
839 /**
840 * @param string $sub
841 * @param array<string, mixed> $tableOptions
842 * @return string
843 */
844 public function buildViewDoneCountQuery(string $sub, array $tableOptions): string {
845 return $this->queryBuilder->buildViewDoneCountQuery($sub, $tableOptions);
846 }
847
848 /** @return array<string, string> */
849 public function viewBuildOnlyTranslations(): array {
850 return $this->queryBuilder->viewBuildOnlyTranslations();
851 }
852
853 // =========================================================================
854 // Delegated: ViewCacheInvalidator
855 // =========================================================================
856
857 /**
858 * @template T
859 * @param callable():T $work
860 * @return T
861 */
862 public function runWithDeferredInvalidation(callable $work) {
863 return $this->cacheInvalidator->runWithDeferredInvalidation($work);
864 }
865
866 /** @return void */
867 function invalidateStatusCountsCache(): void {
868 $this->cacheInvalidator->invalidateStatusCountsCache();
869 }
870
871 /** @return void */
872 function invalidateViewSnapshotCache(): void {
873 $this->cacheInvalidator->invalidateViewSnapshotCache();
874 }
875
876 /** @return void */
877 function clearRegexRedirectsCache(): void {
878 $this->cacheInvalidator->clearRegexRedirectsCache();
879 }
880
881 // =========================================================================
882 // Delegated: ViewDiagnostics
883 // =========================================================================
884
885 /**
886 * @param string $sub
887 * @param string $failedQuery
888 * @param array<string, mixed> $tableOptions
889 * @param array<string, mixed> $queryResult
890 * @return array<string, mixed>
891 */
892 public function captureViewQueryFailureDiagnostics(string $sub, string $failedQuery, array $tableOptions, array $queryResult): array {
893 return $this->diagnostics->captureViewQueryFailureDiagnostics($sub, $failedQuery, $tableOptions, $queryResult);
894 }
895
896 // =========================================================================
897 // Delegated: ViewSnapshotCache
898 // =========================================================================
899
900 /**
901 * @param string $sub
902 * @param array<string, mixed> $tableOptions
903 * @return array<string, mixed>
904 */
905 function warmViewTableSnapshotStage(string $sub, array $tableOptions): array {
906 return $this->snapshotCache->warmViewTableSnapshotStage($sub, $tableOptions);
907 }
908
909 /** @return array<string, int> */
910 public function getViewBuildProgressFingerprint(): array {
911 return $this->snapshotCache->getViewBuildProgressFingerprint();
912 }
913
914 // =========================================================================
915 // ViewMetadata (originally in host, not from traits)
916 // =========================================================================
917
918 /** @return array<string, mixed> */
919 function getTableEngines() {
920 $query = ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/sql/selectTableEngines.sql");
921 $results = $this->dbCore->queryAndGetResults($query);
922 return $results;
923 }
924
925 /** @return bool */
926 function isMyISAMSupported(): bool {
927 $supportResults = $this->dbCore->queryAndGetResults("SELECT ENGINE, SUPPORT " .
928 "FROM information_schema.ENGINES WHERE lower(ENGINE) = 'myisam'",
929 array('log_errors' => false));
930
931 if (!empty($supportResults) && !empty($supportResults['rows']) && is_array($supportResults['rows'])) {
932 $rows = $supportResults['rows'];
933 $row = is_array($rows[0] ?? null) ? $rows[0] : array();
934 $supportValue = array_key_exists('support', $row) ? (string)($row['support'] ?? '') :
935 (array_key_exists('SUPPORT', $row) ? (string)($row['SUPPORT'] ?? '') : "nope");
936
937 return strtolower($supportValue) == 'yes';
938 }
939 return false;
940 }
941
942 /**
943 * @param string $tableName
944 * @param array<string, mixed> $dataToInsert
945 * @return array<string, mixed>
946 */
947 function insertAndGetResults($tableName, $dataToInsert) {
948 $tableName = $this->dbCore->doTableNameReplacements($tableName);
949
950 $columns = array();
951 $placeholders = array();
952 $values = array();
953
954 foreach ($dataToInsert as $column => $value) {
955 $columns[] = '`' . $column . '`';
956
957 if ($value === null) {
958 $placeholders[] = 'NULL';
959 } else {
960 $currentDataType = gettype($value);
961 if ($currentDataType == 'integer' || $currentDataType == 'double') {
962 $placeholders[] = '%d';
963 $values[] = $value;
964 } elseif ($currentDataType == 'boolean') {
965 $placeholders[] = '%d';
966 $values[] = $value ? 1 : 0;
967 } else {
968 $placeholders[] = '%s';
969 $values[] = is_scalar($value) ? (string)$value : '';
970 }
971 }
972 }
973
974 $sql = 'INSERT INTO `' . $tableName . '` (' . implode(', ', $columns) . ') VALUES (' . implode(', ', $placeholders) . ')';
975
976 return $this->dbCore->queryAndGetResults($sql, ['query_params' => $values]);
977 }
978
979 /** @return int */
980 function getCapturedCount() {
981 $query = "select count(id) from {wp_abj404_redirects} where status = " . absint(ABJ404_STATUS_CAPTURED);
982
983 $result = $this->dbCore->queryAndGetResults($query);
984 if (!empty($result['timed_out']) || (isset($result['last_error']) && $result['last_error'] != '')) {
985 return 0;
986 }
987
988 $rows = is_array($result['rows'] ?? null) ? $result['rows'] : array();
989 if (empty($rows)) {
990 return 0;
991 }
992 $first = $rows[0];
993 $value = is_array($first) ? reset($first) : $first;
994 return self::scalarToInt($value);
995 }
996
997 /** @return array<int, string> */
998 function getAllPostTypes() {
999 $query = "SELECT DISTINCT post_type FROM {wp_posts} order by post_type";
1000 $results = $this->dbCore->queryAndGetResults($query);
1001 $rows = $results['rows'];
1002
1003 $postType = array();
1004
1005 if (is_array($rows)) {
1006 foreach ($rows as $row) {
1007 array_push($postType, $row['post_type']);
1008 }
1009 }
1010
1011 return $postType;
1012 }
1013
1014 /** @return int */
1015 function getLogDiskUsage() {
1016 $query = 'SELECT (data_length+index_length) tablesize FROM information_schema.tables '
1017 . 'WHERE table_name=\'{wp_abj404_logsv2}\'';
1018
1019 $result = $this->dbCore->queryAndGetResults($query);
1020
1021 if (!empty($result['timed_out']) || (isset($result['last_error']) && $result['last_error'] != '')) {
1022 $err = isset($result['last_error']) && is_string($result['last_error']) ? $result['last_error'] : '';
1023 if ($err !== '') {
1024 $this->logger->errorMessage("Error: " . esc_html($err));
1025 }
1026 return -1;
1027 }
1028
1029 $rows = is_array($result['rows'] ?? null) ? $result['rows'] : array();
1030 if (empty($rows)) {
1031 return 0;
1032 }
1033
1034 $row = is_array($rows[0] ?? null) ? $rows[0] : array();
1035 $size = $row['tablesize'] ?? null;
1036 if ($size === null || !is_scalar($size)) {
1037 return 0;
1038 }
1039 $bytes = intval($size);
1040 return function_exists('apply_filters')
1041 ? (int) apply_filters('abj404_log_disk_usage', $bytes)
1042 : $bytes;
1043 }
1044
1045 /**
1046 * @param array<int, int> $types
1047 * @param int $trashed
1048 * @return int
1049 */
1050 function getRecordCount($types = array(), $trashed = 0) {
1051 $recordCount = 0;
1052
1053 if (count($types) >= 1) {
1054 $query = "select count(id) as count from {wp_abj404_redirects} where 1 and (status in (";
1055
1056 $filteredTypes = array_map('absint', $types);
1057 $typesForSQL = implode(", ", $filteredTypes);
1058 $query .= $typesForSQL . "))";
1059 $query .= " and disabled = " . absint($trashed);
1060
1061 $result = $this->dbCore->queryAndGetResults($query);
1062 $rows = is_array($result['rows']) ? $result['rows'] : array();
1063 if (!empty($rows)) {
1064 $row = is_array($rows[0] ?? null) ? $rows[0] : array();
1065 $recordCount = isset($row['count']) && is_scalar($row['count']) ? intval($row['count']) : 0;
1066 }
1067 }
1068
1069 return intval($recordCount);
1070 }
1071
1072 // =========================================================================
1073 // ViewQueriesHitsLifecycle
1074 // =========================================================================
1075
1076 /** @return void */
1077 function maybeUpdateRedirectsForViewHitsTable(): void {
1078 $this->dbCore->setRuntimeFlag(self::HITS_TABLE_LAST_CHECKED_FLAG, time(), 86400);
1079
1080 if (function_exists('abj_service')) {
1081 $upgradesEtc = abj_service('database_upgrades');
1082 if (is_object($upgradesEtc) && method_exists($upgradesEtc, 'scheduleLogsv2CanonicalUrlBackfill')) {
1083 $upgradesEtc->scheduleLogsv2CanonicalUrlBackfill();
1084 }
1085 }
1086
1087 if ($this->dbCore->shouldSkipNonEssentialDbWrites()) {
1088 $this->logger->debugMessage(__FUNCTION__ . " skipped due to temporary DB write cooldown.");
1089 $this->dbCore->setRuntimeFlag(self::HITS_TABLE_LAST_DECISION_FLAG, 'paused', 86400);
1090 return;
1091 }
1092
1093 if (!$this->logsRepo->logsHitsTableExists()) {
1094 $this->logger->debugMessage(__FUNCTION__ . " table doesn't exist, deferring creation to shutdown hook.");
1095 $this->logsRepo->scheduleHitsTableRebuild();
1096 return;
1097 }
1098
1099 $this->logsRepo->recordLogsHitsRollupStalenessSignal();
1100
1101 if (!$this->logsRepo->hitsTableNeedsRebuild()) {
1102 $this->dbCore->setRuntimeFlag(self::HITS_TABLE_LAST_DECISION_FLAG, 'not_needed', 86400);
1103 return;
1104 }
1105
1106 $this->logsRepo->scheduleHitsTableRebuild();
1107 }
1108 }
1109