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 / DataAccess.php

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

1,512 lines 72.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3
4 if (!defined('ABSPATH')) {
5 exit;
6 }
7
8 require_once __DIR__ . '/ViewBuildCollaborator.php';
9 require_once __DIR__ . '/ViewQueriesStaged.php';
10 require_once __DIR__ . '/ViewBuildStageCallbacks.php';
11 require_once __DIR__ . '/ViewBuildStageRunner.php';
12 require_once __DIR__ . '/ViewBuildStartedWatermark.php';
13 require_once __DIR__ . '/ViewBuildAdaptive.php';
14 require_once __DIR__ . '/ViewBuildHelpers.php';
15 require_once __DIR__ . '/ViewBuildLockAndCron.php';
16 require_once __DIR__ . '/ViewBuildPhpEnvProbe.php';
17 require_once __DIR__ . '/ViewBuildSessionEnvProbe.php';
18 require_once __DIR__ . '/ViewBuildHostFailurePolicy.php';
19 require_once __DIR__ . '/ViewBuildForceRestart.php';
20 require_once __DIR__ . '/MutationWatermarkSeam.php';
21 require_once __DIR__ . '/AdminMutationGate.php';
22 require_once __DIR__ . '/DatabaseRuntimeState.php';
23 require_once __DIR__ . '/ViewReadRuntimeState.php';
24 require_once __DIR__ . '/DatabaseConnectionManager.php';
25 require_once __DIR__ . '/DatabaseQueryTimeoutManager.php';
26 require_once __DIR__ . '/ViewBuildOrchestratorInterface.php';
27 require_once __DIR__ . '/ViewBuildOrchestrator.php';
28 require_once __DIR__ . '/ViewReadServiceInterface.php';
29 require_once __DIR__ . '/ViewReadService.php';
30 require_once __DIR__ . '/LogsRepositoryInterface.php';
31 require_once __DIR__ . '/LogsRepository.php';
32 require_once __DIR__ . '/StatsRepositoryInterface.php';
33 require_once __DIR__ . '/StatsRepository.php';
34 require_once __DIR__ . '/ContentRepositoryInterface.php';
35 require_once __DIR__ . '/ContentRepository.php';
36 require_once __DIR__ . '/RedirectsRepositoryInterface.php';
37 require_once __DIR__ . '/RedirectsRepository.php';
38 require_once __DIR__ . '/DatabaseErrorClassifier.php';
39 require_once __DIR__ . '/DatabaseSqlErrorReporter.php';
40 require_once __DIR__ . '/ViewQueryFailureException.php';
41 require_once __DIR__ . '/ViewBuildPendingException.php';
42 require_once __DIR__ . '/DatabaseCoreInterface.php';
43 require_once __DIR__ . '/DatabaseCore.php';
44
45 /* Functions in this class should all reference one of the following variables or support functions that do.
46 * $wpdb, $_GET, $_POST, $_SERVER, $_.*
47 * everything $wpdb related.
48 * everything $_GET, $_POST, (etc) related.
49 * Read the database, Store to the database,
50 */
51
52 class ABJ_404_Solution_DataAccess implements ABJ_404_Solution_ContentRepositoryInterface {
53
54 const UPDATE_LOGS_HITS_TABLE_HOOK = 'abj404_updateLogsHitsTableAction';
55
56 const KEY_REDIRECTS_FOR_VIEW_COUNT = 'abj404_redirects-for-view-count';
57
58 /** @var int Maximum age in seconds before hits table is considered stale */
59 const HITS_TABLE_MAX_AGE_SECONDS = 300; // 5 minutes
60 /** @var int Minimum interval between hits-table rebuild schedules (server-side dedupe). */
61 const HITS_TABLE_SCHEDULE_COOLDOWN_SECONDS = 30;
62 /** @var int Short-lived cache for admin list snapshots (fast first paint). */
63 const VIEW_SNAPSHOT_CACHE_TTL_SECONDS = 120;
64 /** @var int Minimum interval between expensive refreshes for the same view key. */
65 const VIEW_SNAPSHOT_REFRESH_COOLDOWN_SECONDS = 30;
66 /** @var int DB timeout budget for each resumable table-cache warmup stage. */
67 const VIEW_SNAPSHOT_WARMUP_STAGE_TIMEOUT_SECONDS = 28;
68 /** @var int Age after which a running warmup stage is treated as killed/stalled. */
69 const VIEW_SNAPSHOT_WARMUP_STALE_SECONDS = 35;
70 /** @var int Max killed/timeout attempts for one warmup stage before blocking retries. */
71 const VIEW_SNAPSHOT_WARMUP_MAX_ATTEMPTS = 3;
72 /** @var int Safety cap: avoid storing extremely large payloads in cache. */
73 const VIEW_SNAPSHOT_MAX_PAYLOAD_BYTES = 2097152; // 2 MiB
74 /** @var int Cross-request lock timeout for logs-hits rebuild jobs. */
75 const HITS_TABLE_REBUILD_LOCK_TTL_SECONDS = 180;
76 /** @var int Number of logsv2 IDs to process per chunk during pre-aggregation. */
77 const HITS_TABLE_PREAGG_CHUNK_SIZE = 100000;
78 /**
79 * @var int If MAX(id) - MIN(id) is at or below this threshold, the rebuild
80 * uses the single-statement direct path; above it, the chunked
81 * two-phase path. Threshold is intentionally far smaller than
82 * HITS_TABLE_PREAGG_CHUNK_SIZE: log retention by timestamp lets
83 * MIN(id) climb monotonically, so MAX-MIN converges to the live
84 * row count, and the direct path's CONCAT/COALESCE-derived JOIN
85 * times out at 60s on shared hosts at row counts well below
86 * HITS_TABLE_PREAGG_CHUNK_SIZE. Only truly tiny tables benefit
87 * from skipping the pre-agg overhead.
88 */
89 const HITS_TABLE_DIRECT_PATH_THRESHOLD = 5000;
90 /** @var int Max age for cached stats-periodic aggregates. */
91 const PERIODIC_STATS_CACHE_TTL_SECONDS = 300;
92 /** @var int Minimum interval before recalculating expensive stats aggregates. */
93 const PERIODIC_STATS_REFRESH_COOLDOWN_SECONDS = 30;
94 /** @var int Max age for cached daily-activity trend data (Stats tab Chart.js). */
95 const TREND_DATA_CACHE_TTL_SECONDS = 900;
96 /**
97 * @var int Short TTL for the cached `getLogsCount(0)` total row count.
98 * Audit F4: InnoDB has no maintained row counter, so the
99 * Logs admin tab's `SELECT COUNT(id) FROM logsv2` is a full
100 * index scan. New inserts move the cache key (`max_log_id`)
101 * so fresh data is picked up immediately; bulk deletes do
102 * not move the key, so the TTL bounds staleness at 60 s.
103 */
104 const LOGS_COUNT_CACHE_TTL_SECONDS = 60;
105 /** @var int Retention for dashboard stats snapshot payload (stale snapshot is acceptable for fast first paint). */
106 const STATS_DASHBOARD_CACHE_TTL_SECONDS = 86400;
107 /** @var int Minimum time between full stats snapshot recomputes. */
108 const STATS_DASHBOARD_REFRESH_COOLDOWN_SECONDS = 30;
109 /** @var int Cooldown when DB query quota is exceeded. */
110 const DB_QUOTA_COOLDOWN_SECONDS = ABJ_404_Solution_DatabaseRuntimeState::DB_QUOTA_COOLDOWN_SECONDS;
111 /** @var int Cooldown when DB is read-only or storage is full. */
112 const DB_WRITE_BLOCK_COOLDOWN_SECONDS = ABJ_404_Solution_DatabaseRuntimeState::DB_WRITE_BLOCK_COOLDOWN_SECONDS;
113
114 /** @var string Runtime flag: last time we checked whether logs-hits needs rebuild (Unix timestamp). */
115 const HITS_TABLE_LAST_CHECKED_FLAG = 'abj404_logs_hits_last_checked_at';
116 /** @var string Runtime flag: last time we scheduled a rebuild (Unix timestamp). */
117 const HITS_TABLE_LAST_SCHEDULED_FLAG = 'abj404_logs_hits_last_scheduled_at';
118 /** @var string Runtime flag: last schedule decision ('scheduled','running','cooldown','paused','not_needed'). */
119 const HITS_TABLE_LAST_DECISION_FLAG = 'abj404_logs_hits_last_decision';
120 /** @var string Runtime flag: last successful hits-table rebuild completion (Unix timestamp). */
121 const HITS_TABLE_LAST_REFRESHED_FLAG = 'abj404_logs_hits_last_refreshed_at';
122 /**
123 * @var string Runtime flag: Unix timestamp of the first request that
124 * observed MAX(logsv2.id) > stored rollup watermark and the
125 * gap has remained open since. Drives the broken-cron
126 * admin notice; cleared on rebuild or when the gap closes.
127 */
128 const HITS_TABLE_FIRST_STALE_DETECTED_FLAG = 'abj404_logs_hits_first_stale_detected_at';
129 /** @var string Deduplicated admin-notice transient for stale logs_hits rollup. */
130 const HITS_TABLE_STALE_NOTICE_TRANSIENT = 'abj404_logs_hits_rollup_stale';
131 /**
132 * @var int Minimum age (seconds) of a persisted MAX(logsv2.id) >
133 * rollup-watermark gap before surfacing a broken-cron admin
134 * notice. 1 hour is well past the normal cron cycle for the
135 * 5-minute HITS_TABLE_MAX_AGE_SECONDS rollup, so a gap that
136 * stays open this long is unambiguously a broken or
137 * stopped cron event (abj404_updateLogsHitsTableAction).
138 */
139 const HITS_TABLE_STALE_NOTICE_THRESHOLD_SECONDS = 3600;
140
141 /** @var self|null */
142 private static $instance = null;
143
144 /** @var ABJ_404_Solution_DatabaseCore The extracted database infrastructure layer. */
145 private $dbCore;
146
147 /** @var ABJ_404_Solution_ContentRepository The extracted content/cache repository. */
148 private $contentRepo;
149
150 /** @var ABJ_404_Solution_RedirectsRepository The extracted redirects repository. */
151 private $redirectsRepo;
152
153 /** @var ABJ_404_Solution_LogsRepository The extracted logs repository. */
154 private $logsRepo;
155
156 /** @var ABJ_404_Solution_StatsRepository The extracted stats repository. */
157 private $statsRepo;
158
159 /** @var ABJ_404_Solution_ViewReadService The extracted view read service (Phase 6). */
160 private $viewReadService;
161
162 /** @var ABJ_404_Solution_ViewBuildOrchestrator The extracted view build orchestrator (Phase 7). */
163 private $viewBuildOrchestrator;
164
165 /** @param bool $value @return void */
166 public static function setViewSnapshotTableEnsured(bool $value): void {
167 ABJ_404_Solution_ViewReadService::setViewSnapshotTableEnsured($value);
168 }
169
170 /**
171 * Delegate to DatabaseCore for backward compatibility.
172 *
173 * @param bool $value
174 * @return void
175 */
176 public static function setSetStatementWrapperUnsupported(bool $value): void {
177 ABJ_404_Solution_DatabaseCore::setSetStatementWrapperUnsupported($value);
178 }
179
180 /** @return bool */
181 public static function isSetStatementWrapperUnsupported(): bool {
182 return ABJ_404_Solution_DatabaseCore::isSetStatementWrapperUnsupported();
183 }
184
185 /** @return void */
186 public static function resetViewBuildOncePerRequestGuard(): void {
187 ABJ_404_Solution_ViewBuildOrchestrator::resetViewBuildOncePerRequestGuard();
188 }
189
190 /** @param string $url @return string */
191 public static function computeRedirectsCanonicalUrl($url): string {
192 return ABJ_404_Solution_RedirectsRepository::computeRedirectsCanonicalUrl($url);
193 }
194
195 /** @param string $columnExpr @return string */
196 public static function hitsCanonicalUrlSqlExpression(string $columnExpr): string {
197 return ABJ_404_Solution_RedirectsRepository::hitsCanonicalUrlSqlExpression($columnExpr);
198 }
199
200 /**
201 * @param string|null $raw
202 * @return array<int, array{step: string, outcome: string, detail: string}>|null
203 */
204 public static function decompressPipelineTrace(?string $raw): ?array {
205 return ABJ_404_Solution_LogsRepository::decompressPipelineTrace($raw);
206 }
207
208 /** @var ABJ_404_Solution_Functions */
209 private $f;
210
211 /** @var ABJ_404_Solution_Logging */
212 private $logger;
213
214 /** Cache key for redirect status counts */
215 const CACHE_KEY_REDIRECT_STATUS = 'abj404_redirect_status_counts';
216
217 /** Cache key for captured status counts */
218 const CACHE_KEY_CAPTURED_STATUS = 'abj404_captured_status_counts';
219
220 /** Cache key for high-impact captured URL count (3+ hits) */
221 const CACHE_KEY_HIGH_IMPACT_CAPTURED = 'abj404_high_impact_captured';
222
223 /** Cache TTL in seconds (24 hours - safety net, primary refresh is event-driven invalidation) */
224 const STATUS_CACHE_TTL = 86400;
225
226 /**
227 * Short-TTL window used after a query timeout to break the
228 * "page reloads, page re-times-out" loop on slow hosts. 5 minutes is
229 * long enough that an admin browsing session does not re-pay the
230 * timeout cost, and short enough that once the scheduled hits-table
231 * rebuild completes, the next request after the window picks up the
232 * rebuilt rollup. See getHighImpactCapturedCount() self-heal branch.
233 */
234 const STATUS_CACHE_TIMEOUT_SELFHEAL_TTL = 300;
235
236 /** Maximum number of regex redirects to cache per-request (memory guard) */
237 const REGEX_CACHE_MAX_COUNT = 50;
238
239 // $regexRedirectsCache and $regexCacheDisabled moved to RedirectsRepository (Phase 2).
240
241 /** @var bool|null Legacy per-request cache for DAO-shaped test subclasses. */
242 private $legacyViewDoneServeableCache = null;
243
244 /** @var array<string, string> Legacy reflection bridge for view-build progress options. */
245 private static $viewBuildProgressOptionNames = array(
246 'started_at' => 'abj404_view_build_started_at',
247 'current_stage' => 'abj404_view_build_current_stage',
248 'last_started_stage' => 'abj404_view_build_last_started_stage',
249 'last_started_at' => 'abj404_view_build_last_started_at',
250 'last_completed_stage' => 'abj404_view_build_last_completed_stage',
251 'last_completed_at' => 'abj404_view_build_last_completed_at',
252 's2_high_water' => 'abj404_view_build_s2_high_water',
253 's4_high_water' => 'abj404_view_build_s4_high_water',
254 's5_high_water' => 'abj404_view_build_s5_high_water',
255 's2_batch_size' => 'abj404_view_build_s2_batch_size',
256 's4_batch_size' => 'abj404_view_build_s4_batch_size',
257 's5_batch_size' => 'abj404_view_build_s5_batch_size',
258 's3_kill_streak' => 'abj404_view_build_s3_kill_streak',
259 's9_kill_streak' => 'abj404_view_build_s9_kill_streak',
260 's10_kill_streak' => 'abj404_view_build_s10_kill_streak',
261 's1_no_progress_streak' => 'abj404_view_build_s1_no_progress',
262 's2_no_progress_streak' => 'abj404_view_build_s2_no_progress',
263 's3_no_progress_streak' => 'abj404_view_build_s3_no_progress',
264 's4_no_progress_streak' => 'abj404_view_build_s4_no_progress',
265 's5_no_progress_streak' => 'abj404_view_build_s5_no_progress',
266 's6_no_progress_streak' => 'abj404_view_build_s6_no_progress',
267 's7_no_progress_streak' => 'abj404_view_build_s7_no_progress',
268 's8_no_progress_streak' => 'abj404_view_build_s8_no_progress',
269 's9_no_progress_streak' => 'abj404_view_build_s9_no_progress',
270 's10_no_progress_streak' => 'abj404_view_build_s10_no_progress',
271 's11_no_progress_streak' => 'abj404_view_build_s11_no_progress',
272 );
273
274 /** @var array<int, array<string, mixed>> Legacy reflection bridge; actual queue is owned by LogsRepository. */
275 private static $logQueue = array();
276 /** @var bool Legacy reflection bridge; actual hook state is owned by LogsRepository. */
277 private static $shutdownHookRegistered = false;
278 /** @var bool Legacy reflection bridge; actual flush state is owned by LogsRepository. */
279 private static $isFlushingLogQueue = false;
280
281 /**
282 * @param ABJ_404_Solution_Functions|null $functions
283 * @param ABJ_404_Solution_Logging|null $logging
284 * @param ABJ_404_Solution_DatabaseCore|null $dbCore
285 * @param ABJ_404_Solution_ContentRepository|null $contentRepo
286 * @param ABJ_404_Solution_RedirectsRepository|null $redirectsRepo
287 * @param ABJ_404_Solution_LogsRepository|null $logsRepo
288 * @param ABJ_404_Solution_StatsRepository|null $statsRepo
289 * @param ABJ_404_Solution_ViewReadService|null $viewReadService
290 * @param ABJ_404_Solution_ViewBuildOrchestrator|null $viewBuildOrchestrator
291 */
292 public function __construct($functions = null, $logging = null, $dbCore = null, $contentRepo = null, $redirectsRepo = null, $logsRepo = null, $statsRepo = null, $viewReadService = null, $viewBuildOrchestrator = null) {
293 $this->f = is_object($functions) && method_exists($functions, 'strtolower') ? $functions : abj_service('functions');
294 $this->logger = is_object($logging) && (method_exists($logging, 'debugMessage') || method_exists($logging, 'errorMessage')) ? $logging : abj_service('logging');
295
296 if ($dbCore !== null) {
297 $this->dbCore = $dbCore;
298 } else if (get_class($this) !== __CLASS__
299 && method_exists($this, 'queryAndGetResults')
300 && (new \ReflectionMethod($this, 'queryAndGetResults'))->getDeclaringClass()->getName() !== __CLASS__) {
301 $owner = $this;
302 $this->dbCore = new class($owner, $this->f, $this->logger) extends ABJ_404_Solution_DatabaseCore {
303 private $owner;
304 public function __construct($owner, $functions, $logger) {
305 $this->owner = $owner;
306 parent::__construct($functions, $logger);
307 }
308 public function queryAndGetResults($query, $options = array()): array {
309 return $this->owner->queryAndGetResults($query, $options);
310 }
311 public function doTableNameReplacements($query): string {
312 if (method_exists($this->owner, 'doTableNameReplacements')
313 && (new \ReflectionMethod($this->owner, 'doTableNameReplacements'))->getDeclaringClass()->getName() !== 'ABJ_404_Solution_DataAccess') {
314 return (string)$this->owner->doTableNameReplacements($query);
315 }
316 return parent::doTableNameReplacements($query);
317 }
318 public function tableExists($tableName): bool {
319 if (method_exists($this->owner, 'tableExists')
320 && (new \ReflectionMethod($this->owner, 'tableExists'))->getDeclaringClass()->getName() !== 'ABJ_404_Solution_DataAccess') {
321 return (bool)$this->owner->tableExists($tableName);
322 }
323 return parent::tableExists($tableName);
324 }
325 public function getLowercasePrefix(): string {
326 if (method_exists($this->owner, 'getLowercasePrefix')
327 && (new \ReflectionMethod($this->owner, 'getLowercasePrefix'))->getDeclaringClass()->getName() !== 'ABJ_404_Solution_DataAccess') {
328 return (string)$this->owner->getLowercasePrefix();
329 }
330 return parent::getLowercasePrefix();
331 }
332 };
333 } else {
334 $this->dbCore = new ABJ_404_Solution_DatabaseCore($this->f, $this->logger);
335 }
336 if ($contentRepo !== null) {
337 $this->contentRepo = $contentRepo;
338 } else {
339 $this->contentRepo = new ABJ_404_Solution_ContentRepository($this->dbCore, $this->f, $this->logger);
340 }
341
342 if ($redirectsRepo !== null) {
343 $this->redirectsRepo = $redirectsRepo;
344 } else {
345 $this->redirectsRepo = new ABJ_404_Solution_RedirectsRepository($this->dbCore, $this->f, $this->logger);
346 }
347
348 if ($logsRepo !== null) {
349 $this->logsRepo = $logsRepo;
350 } else if (get_class($this) !== __CLASS__
351 && ((method_exists($this, 'logsHitsTableExists')
352 && (new \ReflectionMethod($this, 'logsHitsTableExists'))->getDeclaringClass()->getName() !== __CLASS__)
353 || (method_exists($this, 'scheduleHitsTableRebuild')
354 && (new \ReflectionMethod($this, 'scheduleHitsTableRebuild'))->getDeclaringClass()->getName() !== __CLASS__))) {
355 $owner = $this;
356 $this->logsRepo = new class($owner, $this->dbCore, $this->f, $this->logger) extends ABJ_404_Solution_LogsRepository {
357 private $owner;
358 public function __construct($owner, $dbCore, $functions, $logger) {
359 $this->owner = $owner;
360 parent::__construct($dbCore, $functions, $logger);
361 }
362 public function logsHitsTableExists() {
363 return (bool)$this->owner->logsHitsTableExists();
364 }
365 public function scheduleHitsTableRebuild(): void {
366 $this->owner->scheduleHitsTableRebuild();
367 }
368 };
369 } else {
370 $this->logsRepo = new ABJ_404_Solution_LogsRepository($this->dbCore, $this->f, $this->logger);
371 }
372
373 if ($statsRepo !== null) {
374 $this->statsRepo = $statsRepo;
375 } else if (get_class($this) !== __CLASS__
376 && method_exists($this, 'getStatsCount')
377 && (new \ReflectionMethod($this, 'getStatsCount'))->getDeclaringClass()->getName() !== __CLASS__) {
378 $owner = $this;
379 $this->statsRepo = new class($owner, $this->dbCore, $this->logsRepo, $this->f, $this->logger) extends ABJ_404_Solution_StatsRepository {
380 private $owner;
381 public function __construct($owner, $dbCore, $logsRepo, $functions, $logger) {
382 $this->owner = $owner;
383 parent::__construct($dbCore, $logsRepo, $functions, $logger);
384 }
385 public function getStatsCount($query, array $valueParams) {
386 return $this->owner->getStatsCount($query, $valueParams);
387 }
388 };
389 } else {
390 $this->statsRepo = new ABJ_404_Solution_StatsRepository($this->dbCore, $this->logsRepo, $this->f, $this->logger);
391 }
392
393 if ($viewReadService !== null) {
394 $this->viewReadService = $viewReadService;
395 } else {
396 $this->viewReadService = new ABJ_404_Solution_ViewReadService(
397 $this->dbCore, $this->logsRepo, $this->redirectsRepo, $this->f, $this->logger
398 );
399 }
400
401 if ($viewBuildOrchestrator !== null) {
402 $this->viewBuildOrchestrator = $viewBuildOrchestrator;
403 } else if (get_class($this) !== __CLASS__
404 && ((method_exists($this, 'runRedirectsForViewStaged')
405 && (new \ReflectionMethod($this, 'runRedirectsForViewStaged'))->getDeclaringClass()->getName() !== __CLASS__)
406 || (method_exists($this, 'advanceViewBuildOnce')
407 && (new \ReflectionMethod($this, 'advanceViewBuildOnce'))->getDeclaringClass()->getName() !== __CLASS__)
408 || (method_exists($this, 'runPageLoadFallbackAdvance')
409 && (new \ReflectionMethod($this, 'runPageLoadFallbackAdvance'))->getDeclaringClass()->getName() !== __CLASS__)
410 || (method_exists($this, 'viewDoneIsServeable')
411 && (new \ReflectionMethod($this, 'viewDoneIsServeable'))->getDeclaringClass()->getName() !== __CLASS__))) {
412 $owner = $this;
413 $this->viewBuildOrchestrator = new class($owner, $this->dbCore, $this->f, $this->logger) extends ABJ_404_Solution_ViewBuildOrchestrator {
414 private $owner;
415 public function __construct($owner, $dbCore, $functions, $logger) {
416 $this->owner = $owner;
417 parent::__construct($dbCore, $functions, $logger);
418 }
419 public function runRedirectsForViewStaged(string $sub, array $tableOptions): array {
420 return $this->owner->runRedirectsForViewStaged($sub, $tableOptions);
421 }
422 public function runRedirectsForViewCountStaged(string $sub, array $tableOptions): int {
423 return $this->owner->runRedirectsForViewCountStaged($sub, $tableOptions);
424 }
425 public function advanceViewBuildOnce(bool $forceRebuild = false): array {
426 if (method_exists($this->owner, 'advanceViewBuildOnce')
427 && (new \ReflectionMethod($this->owner, 'advanceViewBuildOnce'))->getDeclaringClass()->getName() !== 'ABJ_404_Solution_DataAccess') {
428 return $this->owner->advanceViewBuildOnce($forceRebuild);
429 }
430 return parent::advanceViewBuildOnce($forceRebuild);
431 }
432 public function runPageLoadFallbackAdvance(): array {
433 if (method_exists($this->owner, 'runPageLoadFallbackAdvance')
434 && (new \ReflectionMethod($this->owner, 'runPageLoadFallbackAdvance'))->getDeclaringClass()->getName() !== 'ABJ_404_Solution_DataAccess') {
435 return $this->owner->runPageLoadFallbackAdvance();
436 }
437 return parent::runPageLoadFallbackAdvance();
438 }
439 public function viewDoneIsServeable(): bool {
440 if (method_exists($this->owner, 'viewDoneIsServeable')
441 && (new \ReflectionMethod($this->owner, 'viewDoneIsServeable'))->getDeclaringClass()->getName() !== 'ABJ_404_Solution_DataAccess') {
442 return (bool)$this->owner->viewDoneIsServeable();
443 }
444 return parent::viewDoneIsServeable();
445 }
446 };
447 } else {
448 $this->viewBuildOrchestrator = new ABJ_404_Solution_ViewBuildOrchestrator(
449 $this->dbCore, $this->f, $this->logger, $this->resolveRebuildHealthState()
450 );
451 }
452 $this->viewBuildOrchestrator->setViewReadService($this->viewReadService);
453 $this->viewBuildOrchestrator->setLogsRepository($this->logsRepo);
454 $this->viewReadService->setViewBuildOrchestrator($this->viewBuildOrchestrator);
455 }
456
457 /** @return ABJ_404_Solution_DatabaseCore */
458 public function getDbCore(): ABJ_404_Solution_DatabaseCore {
459 if ($this->dbCore === null) {
460 $this->dbCore = new ABJ_404_Solution_DatabaseCore($this->f, $this->logger);
461 }
462 return $this->dbCore;
463 }
464
465 public function queryAndGetResults($query, $options = array()) {
466 return $this->getDbCore()->queryAndGetResults($query, $options);
467 }
468
469 /** @return ABJ_404_Solution_RebuildHealthState|null */
470 private function resolveRebuildHealthState() {
471 if (class_exists('ABJ_404_Solution_ServiceContainer')
472 && ABJ_404_Solution_ServiceContainer::safeHas('rebuild_health')) {
473 $service = ABJ_404_Solution_ServiceContainer::safeGet('rebuild_health');
474 if ($service instanceof ABJ_404_Solution_RebuildHealthState) {
475 return $service;
476 }
477 }
478 return null;
479 }
480
481 public function queryScalarInt($query, $options = array()): int {
482 return $this->getDbCore()->queryScalarInt($query, $options);
483 }
484
485 public function doTableNameReplacements($query): string {
486 return $this->getDbCore()->doTableNameReplacements($query);
487 }
488
489 public function getLowercasePrefix(): string {
490 return $this->getDbCore()->getLowercasePrefix();
491 }
492
493 public function getPrefixedTableName($tableSuffix): string {
494 return $this->getDbCore()->getPrefixedTableName($tableSuffix);
495 }
496
497 /** @param string $query @return string */
498 public function extractSqlFilename($query): string {
499 return $this->getDbCore()->extractSqlFilename($query);
500 }
501
502 /** @param string $errorText @return bool */
503 public function classifyAndHandleInfrastructureError(string $errorText): bool {
504 return $this->getDbCore()->classifyAndHandleInfrastructureError($errorText);
505 }
506
507 /** @param mixed $errorText @return bool */
508 public function isInvalidDataError($errorText): bool {
509 return $this->getDbCore()->isInvalidDataError($errorText);
510 }
511
512 /** @param string $errorText @return bool */
513 public function isCollationError(string $errorText): bool {
514 return $this->getDbCore()->isCollationError($errorText);
515 }
516
517 /** @return string */
518 public function diagnosePrefixMismatch(): string {
519 return $this->getDbCore()->diagnosePrefixMismatch();
520 }
521
522 /** @param string $errorText @return bool */
523 public function isMultisiteCrossPrefixError(string $errorText): bool {
524 return $this->getDbCore()->isMultisiteCrossPrefixError($errorText);
525 }
526
527 public function isDeadlockOrLockTimeoutError(string $errorText): bool {
528 return $this->getDbCore()->isDeadlockOrLockTimeoutError($errorText);
529 }
530
531 public function isTransientConnectionError(?string $errorText): bool { return $this->getDbCore()->isTransientConnectionError($errorText); }
532 public function isQuotaLimitError(string $errorText): bool { return $this->getDbCore()->isQuotaLimitError($errorText); }
533 public function isDiskFullError(string $errorText): bool { return $this->getDbCore()->isDiskFullError($errorText); }
534 public function isReadOnlyError(string $errorText): bool { return $this->getDbCore()->isReadOnlyError($errorText); }
535 public function isCrashedTableError(string $errorText): bool { return $this->getDbCore()->isCrashedTableError($errorText); }
536 public function isIncorrectKeyFileError(string $errorText): bool { return $this->getDbCore()->isIncorrectKeyFileError($errorText); }
537 public function isGaleraConflictError(string $errorText): bool { return $this->getDbCore()->isGaleraConflictError($errorText); }
538 public function isMissingPluginTableError(string $errorText): bool { return $this->getDbCore()->isMissingPluginTableError($errorText); }
539 public function isTransientViewBuildTableError(string $errorText): bool { return $this->getDbCore()->isTransientViewBuildTableError($errorText); }
540 public function noteDatabaseIssueFromError(string $errorText): void { $this->getDbCore()->noteDatabaseIssueFromError($errorText); }
541 public function isWriteBlockActive(): bool { return $this->getDbCore()->isWriteBlockActive(); }
542 public function isQuotaCooldownActive(): bool { return $this->getDbCore()->isQuotaCooldownActive(); }
543 public function getRuntimeFlag(string $name) { return $this->getDbCore()->getRuntimeFlag($name); }
544 public function setRuntimeFlag(string $name, $value, int $ttlSeconds = 0): void { $this->getDbCore()->setRuntimeFlag($name, $value, $ttlSeconds); }
545 public function setPluginDbNotice(string $type, string $message, string $errorString = ''): void { $this->getDbCore()->setPluginDbNotice($type, $message, $errorString); }
546 public function attemptMissingTableRepairAndRetry($query, array &$result): void { $this->getDbCore()->attemptMissingTableRepairAndRetry($query, $result); }
547
548 public function getPostOrGetSanitize($name, $defaultValue = null) {
549 if (is_object($this->f) && method_exists($this->f, 'getPostOrGetSanitize')) {
550 return $this->f->getPostOrGetSanitize($name, $defaultValue);
551 }
552 $returnValue = isset($_GET[$name]) ? $_GET[$name] : (isset($_POST[$name]) ? $_POST[$name] : null);
553 if ($returnValue === null && $name === 'action') {
554 $returnValue = isset($_GET['abj404action']) ? $_GET['abj404action'] : (isset($_POST['abj404action']) ? $_POST['abj404action'] : null);
555 }
556 if ($returnValue !== null && function_exists('sanitize_text_field')) {
557 $returnValue = is_array($returnValue) ? array_map('sanitize_text_field', $returnValue) : sanitize_text_field($returnValue);
558 }
559 $finalValue = $returnValue ?? $defaultValue;
560 return is_string($finalValue) ? $finalValue : (is_string($defaultValue) ? $defaultValue : '');
561 }
562
563 public function getPostOrGetSanitizeUrl($name, $defaultValue = null) {
564 if (is_object($this->f) && method_exists($this->f, 'getPostOrGetSanitizeUrl')) {
565 return $this->f->getPostOrGetSanitizeUrl($name, $defaultValue);
566 }
567 $returnValue = isset($_GET[$name]) ? $_GET[$name] : (isset($_POST[$name]) ? $_POST[$name] : null);
568 return $returnValue === null ? $defaultValue : $returnValue;
569 }
570
571 /** @return ABJ_404_Solution_ContentRepository */
572 public function getContentRepo(): ABJ_404_Solution_ContentRepository {
573 if ($this->contentRepo === null) {
574 $this->contentRepo = new ABJ_404_Solution_ContentRepository($this->getDbCore(), $this->f, $this->logger);
575 }
576 return $this->contentRepo;
577 }
578
579 public function getPublishedPagesAndPostsIDs($slug = '', $searchTerm = '',
580 $limitResults = '', $orderResults = '', $extraWhereClause = '') {
581 return $this->getContentRepo()->getPublishedPagesAndPostsIDs(
582 $slug, $searchTerm, $limitResults, $orderResults, $extraWhereClause
583 );
584 }
585
586 /** @return array<int, object> */
587 public function getPublishedImagesIDs() {
588 return $this->getContentRepo()->getPublishedImagesIDs();
589 }
590
591 public function getPublishedTags($slug = null, $limit = null) {
592 return $this->getContentRepo()->getPublishedTags($slug, $limit);
593 }
594
595 public function addURLToTermsRows($rows) {
596 return $this->getContentRepo()->addURLToTermsRows($rows);
597 }
598
599 public function getPublishedCategories($term_id = null, $slug = null, $limit = null) {
600 return $this->getContentRepo()->getPublishedCategories($term_id, $slug, $limit);
601 }
602
603 public function truncatePermalinkCacheTable(): void {
604 $this->getContentRepo()->truncatePermalinkCacheTable();
605 }
606
607 public function removeFromPermalinkCache(int $post_id): void {
608 $this->getContentRepo()->removeFromPermalinkCache($post_id);
609 }
610
611 public function getPermalinkFromCache($id) {
612 return $this->getContentRepo()->getPermalinkFromCache($id);
613 }
614
615 public function getPermalinksByIds(array $ids) {
616 return $this->getContentRepo()->getPermalinksByIds($ids);
617 }
618
619 public function getPermalinkEtcFromCache($id) {
620 return $this->getContentRepo()->getPermalinkEtcFromCache($id);
621 }
622
623 public function getIDsNeededForPermalinkCache() {
624 return $this->getContentRepo()->getIDsNeededForPermalinkCache();
625 }
626
627 public function storeSpellingPermalinksToCache(string $requestedURLRaw, $returnValue): void {
628 $this->getContentRepo()->storeSpellingPermalinksToCache($requestedURLRaw, $returnValue);
629 }
630
631 public function getSpellingPermalinksFromCache(string $requestedURLRaw) {
632 return $this->getContentRepo()->getSpellingPermalinksFromCache($requestedURLRaw);
633 }
634
635 public function deleteSpellingCache(): void {
636 $this->getContentRepo()->deleteSpellingCache();
637 }
638
639 public function getOldSlug($post_id) {
640 return $this->getContentRepo()->getOldSlug($post_id);
641 }
642
643 public function updatePermalinkCache() {
644 return $this->getContentRepo()->updatePermalinkCache();
645 }
646
647 public function updatePermalinkCacheParentPages() {
648 return $this->getContentRepo()->updatePermalinkCacheParentPages();
649 }
650
651 public function getPermalinkCacheCount(): int {
652 return $this->getContentRepo()->getPermalinkCacheCount();
653 }
654
655 /** @return ABJ_404_Solution_RedirectsRepository */
656 public function getRedirectsRepo(): ABJ_404_Solution_RedirectsRepository {
657 if ($this->redirectsRepo === null) {
658 $this->redirectsRepo = new ABJ_404_Solution_RedirectsRepository($this->getDbCore(), $this->f, $this->logger);
659 }
660 return $this->redirectsRepo;
661 }
662
663 /** @return int */
664 public function cleanupOrphanedAutoRedirects(): int {
665 return $this->getRedirectsRepo()->cleanupOrphanedAutoRedirects();
666 }
667
668 public function deleteRedirect($id) {
669 return $this->getRedirectsRepo()->deleteRedirect($id);
670 }
671
672 public function setupRedirect($fromURL, $status, $type, $final_dest, $code, $disabled = 0, $engine = null, $score = null) {
673 return $this->getRedirectsRepo()->setupRedirect($fromURL, $status, $type, $final_dest, $code, $disabled, $engine, $score);
674 }
675
676 public function getActiveRedirectForURL($url, $degradedMode = false) {
677 if (get_class($this) !== __CLASS__
678 && (method_exists($this, 'prepare_query_wp') || method_exists($this, 'queryAndGetResults'))) {
679 $url = $this->f->sanitizeInvalidUTF8($url);
680 if (function_exists('mb_check_encoding') && !mb_check_encoding($url, 'UTF-8')) {
681 return array('id' => 0);
682 }
683 $logic = abj_service('plugin_logic');
684 $candidates = is_object($logic) && method_exists($logic, 'getNormalizedUrlCandidates')
685 ? $logic->getNormalizedUrlCandidates($url)
686 : array($url);
687 foreach ($candidates as $candidate) {
688 $url1 = $candidate;
689 $url2 = substr($candidate, -1) === '/' ? rtrim($candidate, '/') : $candidate . '/';
690 $query = ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/sql/getPermalinkFromURL.sql");
691 $query = $this->prepare_query_wp($query, array("url1" => $url1, "url2" => $url2));
692 $query = $this->doTableNameReplacements($query);
693 $query = $this->f->doNormalReplacements($query);
694 $results = $this->queryAndGetResults($query);
695 $rows = is_array($results['rows'] ?? null) ? $results['rows'] : array();
696 if (!empty($rows)) {
697 $redirect = array();
698 foreach ($rows[0] as $key => $value) {
699 $redirect[$key] = $value;
700 }
701 if (!isset($redirect['id'])) {
702 $redirect['id'] = 0;
703 }
704 return $redirect;
705 }
706 }
707 return array('id' => 0);
708 }
709 return $this->getRedirectsRepo()->getActiveRedirectForURL($url, $degradedMode);
710 }
711
712 public function getExistingRedirectForURL($url) {
713 return $this->getRedirectsRepo()->getExistingRedirectForURL($url);
714 }
715
716 public function deleteSpecifiedRedirects() {
717 return $this->getRedirectsRepo()->deleteSpecifiedRedirects();
718 }
719
720 public function getRedirectConditions(int $redirectId): array {
721 return $this->getRedirectsRepo()->getRedirectConditions($redirectId);
722 }
723
724 public function saveRedirectConditions(int $redirectId, array $conditions): void {
725 $this->getRedirectsRepo()->saveRedirectConditions($redirectId, $conditions);
726 }
727
728 public function updateRedirect($type, $dest, $fromURL, $idForUpdate, $redirectCode, $statusType, $startTs = null, $endTs = null) {
729 return $this->getRedirectsRepo()->updateRedirect(
730 $type,
731 $dest,
732 $fromURL,
733 $idForUpdate,
734 $redirectCode,
735 $statusType,
736 $startTs,
737 $endTs
738 );
739 }
740
741 public function getRedirectsByIDs($ids) {
742 return $this->getRedirectsRepo()->getRedirectsByIDs($ids);
743 }
744
745 public function updateRedirectTypeStatus($id, $newstatus) {
746 return $this->getRedirectsRepo()->updateRedirectTypeStatus($id, $newstatus);
747 }
748
749 public function moveRedirectsToTrash($id, $trash) {
750 return $this->getRedirectsRepo()->moveRedirectsToTrash($id, $trash);
751 }
752
753 public function deleteOldRedirectsCron() {
754 return $this->getRedirectsRepo()->deleteOldRedirectsCron();
755 }
756
757 public function limitDebugFileSize(): bool {
758 return $this->getRedirectsRepo()->limitDebugFileSize();
759 }
760
761 public function removeDuplicatesCron(): int {
762 return $this->getRedirectsRepo()->removeDuplicatesCron();
763 }
764
765 public function autoTrashJunkCapturedUrls(array $options): int {
766 return $this->getRedirectsRepo()->autoTrashJunkCapturedUrls($options);
767 }
768
769 /** @return ABJ_404_Solution_LogsRepository */
770 public function getLogsRepo(): ABJ_404_Solution_LogsRepository {
771 if ($this->logsRepo === null) {
772 $this->logsRepo = new ABJ_404_Solution_LogsRepository($this->getDbCore(), $this->f, $this->logger);
773 }
774 return $this->logsRepo;
775 }
776
777 public function isTableFullError(string $error): bool { return $this->getLogsRepo()->isTableFullError($error); }
778
779 public function autoTrimLogsv2IfNeeded(string $tableName, string $errorMessage): bool {
780 return $this->getLogsRepo()->autoTrimLogsv2IfNeeded($tableName, $errorMessage);
781 }
782
783 public function getIsolatedWpdb() { return $this->getLogsRepo()->getIsolatedWpdb(); }
784
785 public function isInnoDBTable(string $tableName): bool { return $this->getDbCore()->isInnoDBTable($tableName); }
786
787 public function getLogRecords($tableOptions) { return $this->getLogsRepo()->getLogRecords($tableOptions); }
788
789 public function sanitizeLogEntry(array $entry): ?array { return $this->getLogsRepo()->sanitizeLogEntry($entry); }
790
791 public function populateLogsData($rows) { return $this->getLogsRepo()->populateLogsData($rows); }
792
793 public function getDistinctLoggedUrls(): array { return $this->getLogsRepo()->getDistinctLoggedUrls(); }
794
795 public function getLogsIDandURL($specificURL = '') { return $this->getLogsRepo()->getLogsIDandURL($specificURL); }
796
797 public function getLogsIDandURLLike($specificURL, $limitResults) {
798 return $this->getLogsRepo()->getLogsIDandURLLike($specificURL, $limitResults);
799 }
800
801 public function queueLogEntry(array $entry): void { $this->getLogsRepo()->queueLogEntry($entry); }
802
803 public function flushLogQueue(): void { $this->getLogsRepo()->flushLogQueue(); }
804
805 public function insertLookupValueAndGetID($valueToInsert) { return $this->getLogsRepo()->insertLookupValueAndGetID($valueToInsert); }
806
807 public function getLookupIDForUser($userName) { return $this->getLogsRepo()->getLookupIDForUser($userName); }
808
809 public function correctDuplicateLookupValues(): void { $this->getLogsRepo()->correctDuplicateLookupValues(); }
810
811 public function getDailyActivityTrend(int $days = 30): array { return $this->getLogsRepo()->getDailyActivityTrend($days); }
812
813 public function logsHitsTableExists() { return $this->getLogsRepo()->logsHitsTableExists(); }
814
815 public function createRedirectsForViewHitsTable(): bool { return $this->getLogsRepo()->createRedirectsForViewHitsTable(); }
816
817 public function scheduleHitsTableRebuild(): void { $this->getLogsRepo()->scheduleHitsTableRebuild(); }
818
819 public function getLogsHitsTableLastUpdated() { return $this->getLogsRepo()->getLogsHitsTableLastUpdated(); }
820
821 public function getLogsHitsTableLastUpdatedHuman() { return $this->getLogsRepo()->getLogsHitsTableLastUpdatedHuman(); }
822
823 public function hitsTableNeedsRebuild() { return $this->getLogsRepo()->hitsTableNeedsRebuild(); }
824
825 public function getMaxLogId() { return $this->getLogsRepo()->getMaxLogId(); }
826
827 public function getMinLogId() { return $this->getLogsRepo()->getMinLogId(); }
828
829 public function getStoredMaxLogId() { return $this->getLogsRepo()->getStoredMaxLogId(); }
830
831 /** @return ABJ_404_Solution_StatsRepository */
832 public function getStatsRepo(): ABJ_404_Solution_StatsRepository {
833 if ($this->statsRepo === null) {
834 $this->statsRepo = new ABJ_404_Solution_StatsRepository($this->getDbCore(), $this->getLogsRepo(), $this->f, $this->logger);
835 }
836 return $this->statsRepo;
837 }
838
839 public function getStatsCount($query, array $valueParams) {
840 return $this->getStatsRepo()->getStatsCount($query, $valueParams);
841 }
842
843 public function getPeriodicStatsSummary($sinceTimestamp, $notFoundDest = '404') {
844 return $this->getStatsRepo()->getPeriodicStatsSummary($sinceTimestamp, $notFoundDest);
845 }
846
847 public function getPeriodicStatsSummariesCached($notFoundDest = '404') {
848 return $this->getStatsRepo()->getPeriodicStatsSummariesCached($notFoundDest);
849 }
850
851 public function getStatsDashboardSnapshot($allowStale = true) {
852 return $this->getStatsRepo()->getStatsDashboardSnapshot($allowStale);
853 }
854
855 public function refreshStatsDashboardSnapshot($force = false) {
856 return $this->getStatsRepo()->refreshStatsDashboardSnapshot($force);
857 }
858
859 public function getEarliestLogTimestamp() {
860 return $this->getStatsRepo()->getEarliestLogTimestamp();
861 }
862
863 public function getTopCapturedForDigest(int $limit): array {
864 return $this->getStatsRepo()->getTopCapturedForDigest($limit);
865 }
866
867 public function buildTopCapturedForDigestQuery(int $limit): string {
868 return $this->getStatsRepo()->buildTopCapturedForDigestQuery($limit);
869 }
870
871 public function getDigestSummaryStats(): array {
872 return $this->getStatsRepo()->getDigestSummaryStats();
873 }
874
875 public function getCapturedCountForNotification(): int {
876 return $this->getStatsRepo()->getCapturedCountForNotification();
877 }
878
879 public function getPostsNeedingContentKeywords(int $limit = 500): array {
880 return $this->getStatsRepo()->getPostsNeedingContentKeywords($limit);
881 }
882
883 public function bulkUpdateContentKeywords(array $idToKeywords): void {
884 $this->getStatsRepo()->bulkUpdateContentKeywords($idToKeywords);
885 }
886
887 /** @return ABJ_404_Solution_ViewReadService */
888 public function getViewReadService(): ABJ_404_Solution_ViewReadService {
889 if ($this->viewReadService === null) {
890 $this->viewReadService = new ABJ_404_Solution_ViewReadService(
891 $this->getDbCore(), $this->getLogsRepo(), $this->getRedirectsRepo(), $this->f, $this->logger
892 );
893 if ($this->viewBuildOrchestrator !== null) {
894 $this->viewReadService->setViewBuildOrchestrator($this->viewBuildOrchestrator);
895 }
896 }
897 return $this->viewReadService;
898 }
899
900 /** @return ABJ_404_Solution_ViewBuildOrchestrator */
901 public function getViewBuildOrchestrator(): ABJ_404_Solution_ViewBuildOrchestrator {
902 if ($this->viewBuildOrchestrator === null) {
903 $this->viewBuildOrchestrator = new ABJ_404_Solution_ViewBuildOrchestrator(
904 $this->getDbCore(), $this->f, $this->logger, $this->resolveRebuildHealthState()
905 );
906 $this->viewBuildOrchestrator->setViewReadService($this->getViewReadService());
907 $this->viewBuildOrchestrator->setLogsRepository($this->getLogsRepo());
908 }
909 return $this->viewBuildOrchestrator;
910 }
911
912 /** @return void */
913 public function claimForegroundViewBuildLease(): void { $this->viewBuildOrchestrator->claimForegroundViewBuildLease(); }
914
915 /**
916 * @param string $sub
917 * @param array<string, mixed> $tableOptions
918 * @return array<int, array<string, mixed>>
919 */
920 public function runRedirectsForViewStaged(string $sub, array $tableOptions): array { return $this->viewBuildOrchestrator->runRedirectsForViewStaged($sub, $tableOptions); }
921
922 public function getRedirectStatusCounts($bypassCache = false): array {
923 return $this->getViewReadService()->getRedirectStatusCounts($bypassCache);
924 }
925
926 public function getCapturedStatusCounts($bypassCache = false): array {
927 return $this->getViewReadService()->getCapturedStatusCounts($bypassCache);
928 }
929
930 public function getHighImpactCapturedCount(): int {
931 return $this->getViewReadService()->getHighImpactCapturedCount();
932 }
933
934 public function getLogsCount($logID) {
935 return $this->getViewReadService()->getLogsCount($logID);
936 }
937
938 public function getRedirectsAll() {
939 return $this->getViewReadService()->getRedirectsAll();
940 }
941
942 public function getRedirectsWithLogs() {
943 return $this->getViewReadService()->getRedirectsWithLogs();
944 }
945
946 public function getRedirectsWithRegEx() {
947 return $this->getViewReadService()->getRedirectsWithRegEx();
948 }
949
950 public function getManualRedirectsWithRegexMetachars() {
951 return $this->getViewReadService()->getManualRedirectsWithRegexMetachars();
952 }
953
954 public function getRedirectsForView($sub, $tableOptions) {
955 return $this->getViewReadService()->getRedirectsForView($sub, $tableOptions);
956 }
957
958 public function getRedirectsForViewCount(string $sub, array $tableOptions): int {
959 return $this->getViewReadService()->getRedirectsForViewCount($sub, $tableOptions);
960 }
961
962 public function getRedirectsForViewQuery($sub, $tableOptions, $queryAllRowsAtOnce, $limitStart, $limitEnd, $selectCountOnly) {
963 return $this->getViewReadService()->getRedirectsForViewQuery(
964 $sub,
965 $tableOptions,
966 $queryAllRowsAtOnce,
967 $limitStart,
968 $limitEnd,
969 $selectCountOnly
970 );
971 }
972
973 public function getTableEngines() { return $this->getViewReadService()->getTableEngines(); }
974
975 public function invalidateStatusCountsCache(): void {
976 $this->getViewReadService()->invalidateStatusCountsCache();
977 }
978
979 public function invalidateViewSnapshotCache(): void {
980 $this->getViewReadService()->invalidateViewSnapshotCache();
981 }
982
983 /** @return bool */
984 public function viewDoneIsServeable(): bool {
985 if (get_class($this) !== __CLASS__ && method_exists($this, 'queryAndGetResults')) {
986 if ($this->legacyViewDoneServeableCache !== null) {
987 return $this->legacyViewDoneServeableCache;
988 }
989 $table = $this->getDbCore()->doTableNameReplacements('{wp_abj404_view_done}');
990 $tableCheck = $this->queryAndGetResults("SHOW TABLES LIKE '" . $table . "'", array('log_errors' => false));
991 if (empty($tableCheck['rows'])) {
992 $this->legacyViewDoneServeableCache = false;
993 return $this->legacyViewDoneServeableCache;
994 }
995
996 $observed = function_exists('get_option') ? (int)get_option($this->mutationWatermarkObservedByAdminActionOptionName(), 0) : 0;
997 $observedAt = function_exists('get_option') ? (int)get_option($this->mutationWatermarkObservedByAdminActionAtOptionName(), 0) : 0;
998 $built = function_exists('get_option') ? (int)get_option($this->builtWatermarkOptionName(), 0) : 0;
999 $sanity = defined('ABJ_404_Solution_ViewBuildConfig::VIEW_DONE_MUTATION_INVALIDATED_SANITY_SECONDS')
1000 ? ABJ_404_Solution_ViewBuildConfig::VIEW_DONE_MUTATION_INVALIDATED_SANITY_SECONDS
1001 : 300;
1002 if ($observed > 0 && $built < $observed && $observedAt > 0 && (time() - $observedAt) <= $sanity) {
1003 $this->legacyViewDoneServeableCache = false;
1004 return $this->legacyViewDoneServeableCache;
1005 }
1006
1007 $rowCheck = $this->queryAndGetResults("SELECT 1 FROM `" . $table . "` LIMIT 1", array('log_errors' => false));
1008 if (!empty($rowCheck['rows'])) {
1009 $this->legacyViewDoneServeableCache = true;
1010 return $this->legacyViewDoneServeableCache;
1011 }
1012 $builtAt = function_exists('get_option') ? (int)get_option($this->viewDoneDataBuiltAtOptionName(), 0) : 0;
1013 $this->legacyViewDoneServeableCache = $builtAt > 0;
1014 return $this->legacyViewDoneServeableCache;
1015 }
1016 return $this->viewBuildOrchestrator->viewDoneIsServeable();
1017 }
1018
1019 /** @return int */
1020 public function getViewDoneBuiltAtTimestamp(): int { return $this->viewBuildOrchestrator->getViewDoneBuiltAtTimestamp(); }
1021
1022 /** @return void */
1023 public function markViewDoneBuildCompleted(): void { $this->legacyViewDoneServeableCache = null; $this->viewBuildOrchestrator->markViewDoneBuildCompleted(); }
1024
1025 /** @return array<string, mixed> */
1026 public function getViewBuildProgress(): array { return $this->viewBuildOrchestrator->getViewBuildProgress(); }
1027
1028 /**
1029 * @param bool $forceRebuild
1030 * @return array<string, mixed>
1031 */
1032 public function advanceViewBuildOnce(bool $forceRebuild = false): array { return $this->viewBuildOrchestrator->advanceViewBuildOnce($forceRebuild); }
1033
1034 /** @return array{ran:bool, reason:string, progress:array<string,mixed>} */
1035 public function runPageLoadFallbackAdvance(): array {
1036 if (get_class($this) !== __CLASS__
1037 && method_exists($this, 'advanceViewBuildOnce')
1038 && (new \ReflectionMethod($this, 'advanceViewBuildOnce'))->getDeclaringClass()->getName() !== __CLASS__) {
1039 if ($this->viewBuildOrchestrator->getCronStuckHours() < 24) { return array('ran' => false, 'reason' => 'cron_healthy', 'progress' => $this->getViewBuildProgress()); }
1040 if ($this->viewDoneIsServeable()) { return array('ran' => false, 'reason' => 'not_needed', 'progress' => $this->getViewBuildProgress()); }
1041 $haveTransientApi = function_exists('get_transient') && function_exists('set_transient');
1042 $gateKey = ABJ_404_Solution_ViewBuildConfig::PAGE_LOAD_FALLBACK_GATE_KEY;
1043 if ($haveTransientApi && get_transient($gateKey) !== false) { return array('ran' => false, 'reason' => 'gate_active', 'progress' => $this->getViewBuildProgress()); }
1044 if ($haveTransientApi) { set_transient($gateKey, 1, (int)ABJ_404_Solution_ViewBuildConfig::PAGE_LOAD_FALLBACK_GATE_SECONDS); }
1045 $budgetSeconds = (float)ABJ_404_Solution_ViewBuildConfig::PAGE_LOAD_FALLBACK_BUDGET_SECONDS;
1046 $budgetFilter = static function ($incoming) use ($budgetSeconds) {
1047 $value = is_scalar($incoming) ? (float)$incoming : $budgetSeconds;
1048 return min($value, $budgetSeconds);
1049 };
1050 $filterRegistered = false;
1051 if (function_exists('add_filter')) { add_filter('abj404_view_build_per_stage_budget_seconds', $budgetFilter, 100); $filterRegistered = true; }
1052 try {
1053 $progress = $this->advanceViewBuildOnce(false);
1054 } finally {
1055 if ($filterRegistered && function_exists('remove_filter')) { remove_filter('abj404_view_build_per_stage_budget_seconds', $budgetFilter, 100); }
1056 }
1057 return array('ran' => true, 'reason' => !empty($progress['locked']) ? 'locked' : 'advanced', 'progress' => $progress);
1058 }
1059 return $this->viewBuildOrchestrator->runPageLoadFallbackAdvance();
1060 }
1061
1062 /**
1063 * @param string $sub
1064 * @param array<string, mixed> $tableOptions
1065 * @return int
1066 */
1067 public function runRedirectsForViewCountStaged(string $sub, array $tableOptions): int { return $this->viewBuildOrchestrator->runRedirectsForViewCountStaged($sub, $tableOptions); }
1068
1069 /** @return void */
1070 public function rebuildViewDoneInBackground(): void { $this->viewBuildOrchestrator->rebuildViewDoneInBackground(); }
1071
1072 /** @return string */
1073 public function reconcileStagedTablesAtRunnerStartup(): string { return $this->viewBuildOrchestrator->reconcileStagedTablesAtRunnerStartup(); }
1074
1075 /**
1076 * @param string $optionName
1077 * @param mixed $expected
1078 * @return bool
1079 */
1080 public function verifyOptionWriteCoherent(string $optionName, $expected): bool { return $this->viewBuildOrchestrator->verifyOptionWriteCoherent($optionName, $expected); }
1081
1082 /** @return void */
1083 public function capturePrefixAtBuildStart(): void { $this->viewBuildOrchestrator->capturePrefixAtBuildStart(); }
1084
1085 /** @return bool */
1086 public function verifyPrefixUnchangedSinceStageOne(): bool { return $this->viewBuildOrchestrator->verifyPrefixUnchangedSinceStageOne(); }
1087
1088 /** @return void */
1089 public function clearPrefixAtStageOne(): void { $this->viewBuildOrchestrator->clearPrefixAtStageOne(); }
1090
1091 /** @return array<string, mixed> */
1092 public function probeSqlModeForBuild(): array { return $this->viewBuildOrchestrator->probeSqlModeForBuild(); }
1093
1094 /** @return array<string, mixed> */
1095 public function detectAndAdjustSqlMode(): array { return $this->viewBuildOrchestrator->detectAndAdjustSqlMode(); }
1096
1097 /** @param string $url @param int $maxLength @return string */
1098 public function sanitizeUrlBeforeInsert(string $url, int $maxLength = 0): string { return $this->viewBuildOrchestrator->sanitizeUrlBeforeInsert($url, $maxLength); }
1099
1100 /** @return bool */
1101 public function verifyBuildLockSerializesWriter(): bool { return $this->viewBuildOrchestrator->verifyBuildLockSerializesWriter(); }
1102
1103 /** @param int $delaySeconds @return void */
1104 public function scheduleViewDoneRebuild(int $delaySeconds = 1): void { $this->viewBuildOrchestrator->scheduleViewDoneRebuild($delaySeconds); }
1105
1106 /** @return array<string, mixed> */
1107 public function probePhpEnvironmentForBuild(): array { return $this->viewBuildOrchestrator->probePhpEnvironmentForBuild(); }
1108
1109 /** @return bool */
1110 public function probeSetTimeLimitAvailability(): bool { return $this->viewBuildOrchestrator->probeSetTimeLimitAvailability(); }
1111
1112 /** @return int */
1113 public function probeMemoryLimitForS9(): int { return $this->viewBuildOrchestrator->probeMemoryLimitForS9(); }
1114
1115 /** @return array<string, mixed> */
1116 public function probeFilesystemEnvironmentForBuild(): array { return $this->viewBuildOrchestrator->probeFilesystemEnvironmentForBuild(); }
1117
1118 /** @return void */
1119 public function clearStagedBuildDegradedState(): void { $this->viewBuildOrchestrator->clearStagedBuildDegradedState(); }
1120
1121 /** @return bool */
1122 public function reconcilePostStageElevenState(): bool { return $this->viewBuildOrchestrator->reconcilePostStageElevenState(); }
1123
1124 /** @return array<string, mixed> */
1125 public function probeSessionVariablesAtS1Entry(): array { return $this->viewBuildOrchestrator->probeSessionVariablesAtS1Entry(); }
1126
1127 /** @return void */
1128 public function markViewDoneInvalidatedByAdminMutation(): void { $this->legacyViewDoneServeableCache = null; $this->viewBuildOrchestrator->markViewDoneInvalidatedByAdminMutation(); }
1129
1130 /** @param int $lockTimeoutSeconds @return bool */
1131 public function forceRestartViewBuild(int $lockTimeoutSeconds = 10): bool { return $this->viewBuildOrchestrator->forceRestartViewBuild($lockTimeoutSeconds); }
1132
1133 /** @return int */
1134 public function bumpMutationWatermark(): int { return $this->viewBuildOrchestrator->bumpMutationWatermark(); }
1135
1136 /** @return void */
1137 public function invalidateViewDoneServeableCacheBridge(): void { $this->viewBuildOrchestrator->invalidateViewDoneServeableCacheBridge(); }
1138
1139 public function invalidateViewDoneServeableCache(): void { $this->viewBuildOrchestrator->invalidateViewDoneServeableCacheBridge(); }
1140
1141 public function classifyAndHandleStageFailure(int $stageNumber, string $stageKey, string $errMsg, float $started): string {
1142 return $this->viewBuildOrchestrator->classifyAndHandleStageFailure($stageNumber, $stageKey, $errMsg, $started);
1143 }
1144
1145 public function stageInsertRedirectsBatched(): bool { return $this->viewBuildOrchestrator->stageInsertRedirectsBatched(); }
1146 public function stageUpdatePostsBatched(): bool { return $this->viewBuildOrchestrator->stageUpdatePostsBatched(); }
1147 public function stageUpdateTermsBatched(): bool { return $this->viewBuildOrchestrator->stageUpdateTermsBatched(); }
1148 public function stageUpdateHome(): void { $this->viewBuildOrchestrator->stageUpdateHome(); }
1149 public function runStagedSqlFile(string $relativePath, array $extraTranslations = array()): void { $this->viewBuildOrchestrator->runStagedSqlFile($relativePath, $extraTranslations); }
1150 public function runTimedViewBuildStage(int $stageNumber, string $stageKey, callable $callback) { return $this->viewBuildOrchestrator->runTimedViewBuildStage($stageNumber, $stageKey, $callback); }
1151 public function isStageMarkedSkipped(int $stageNumber): bool { return $this->viewBuildOrchestrator->isStageMarkedSkipped($stageNumber); }
1152
1153 public function normalizeViewWarmupState($state): array {
1154 $default = array(
1155 'status' => 'idle',
1156 'stage' => 'rows',
1157 'stage_started_at' => 0,
1158 'stage_completed_at' => 0,
1159 'attempts_by_stage' => array('rows' => 0, 'count' => 0),
1160 'timings_by_stage' => array(
1161 'rows' => array('last_ms' => 0, 'max_ms' => 0, 'last_completed_at' => 0, 'last_error' => ''),
1162 'count' => array('last_ms' => 0, 'max_ms' => 0, 'last_completed_at' => 0, 'last_error' => ''),
1163 ),
1164 );
1165 if (!is_array($state)) {
1166 return $default;
1167 }
1168 $out = array_merge($default, $state);
1169 $attempts = is_array($out['attempts_by_stage']) ? $out['attempts_by_stage'] : array();
1170 $out['attempts_by_stage'] = array(
1171 'rows' => is_scalar($attempts['rows'] ?? 0) ? intval($attempts['rows'] ?? 0) : 0,
1172 'count' => is_scalar($attempts['count'] ?? 0) ? intval($attempts['count'] ?? 0) : 0,
1173 );
1174 $timings = is_array($out['timings_by_stage']) ? $out['timings_by_stage'] : array();
1175 $out['timings_by_stage'] = array(
1176 'rows' => $this->normalizeViewWarmupStageTiming($timings['rows'] ?? null),
1177 'count' => $this->normalizeViewWarmupStageTiming($timings['count'] ?? null),
1178 );
1179 return $out;
1180 }
1181
1182 private function normalizeViewWarmupStageTiming($timing): array {
1183 $default = array('last_ms' => 0, 'max_ms' => 0, 'last_completed_at' => 0, 'last_error' => '');
1184 if (!is_array($timing)) {
1185 return $default;
1186 }
1187 $out = array_merge($default, $timing);
1188 $out['last_ms'] = is_scalar($out['last_ms']) ? intval($out['last_ms']) : 0;
1189 $out['max_ms'] = is_scalar($out['max_ms']) ? intval($out['max_ms']) : 0;
1190 $out['last_completed_at'] = is_scalar($out['last_completed_at']) ? intval($out['last_completed_at']) : 0;
1191 $out['last_error'] = is_string($out['last_error']) ? $out['last_error'] : '';
1192 return $out;
1193 }
1194
1195 /** @return array<string, mixed> */
1196 public function getStagedQueryOptionsForRead(): array { return $this->viewBuildOrchestrator->getStagedQueryOptionsForRead(); }
1197
1198 /** @param string $shortName @param int $default @return int */
1199 public function readBuildProgressOption(string $shortName, int $default = 0): int { return $this->viewBuildOrchestrator->readBuildProgressOption($shortName, $default); }
1200
1201 public function readProgressOption(string $shortName, int $default = 0): int { return $this->viewBuildOrchestrator->readProgressOption($shortName, $default); }
1202
1203 public function viewBuildPerStageBudgetSeconds(): float { return $this->viewBuildOrchestrator->viewBuildPerStageBudgetSeconds(); }
1204
1205 public function optionReadBackMatches($actual, $expected): bool { return $this->viewBuildOrchestrator->optionReadBackMatches($actual, $expected); }
1206
1207 public function viewDoneFreshnessOptionName(): string { return $this->viewBuildOrchestrator->viewDoneFreshnessOptionName(); }
1208
1209 public function viewDoneDataBuiltAtOptionName(): string { return $this->viewBuildOrchestrator->viewDoneDataBuiltAtOptionName(); }
1210
1211 public function viewDoneMutationInvalidatedAtOptionName(): string { return $this->viewBuildOrchestrator->viewDoneMutationInvalidatedAtOptionName(); }
1212
1213 public function builtWatermarkOptionName(): string { return $this->viewBuildOrchestrator->builtWatermarkOptionName(); }
1214
1215 public function mutationWatermarkObservedByAdminActionOptionName(): string { return $this->viewBuildOrchestrator->mutationWatermarkObservedByAdminActionOptionName(); }
1216
1217 public function mutationWatermarkObservedByAdminActionAtOptionName(): string { return $this->viewBuildOrchestrator->mutationWatermarkObservedByAdminActionAtOptionName(); }
1218
1219 /**
1220 * Backward-compatibility bridge for facade delegations removed in Phase 8e.
1221 * Routes method calls to the extracted sub-service that owns them.
1222 *
1223 * @param string $name
1224 * @param array<int, mixed> $arguments
1225 * @return mixed
1226 * @throws \BadMethodCallException
1227 */
1228 public function __call(string $name, array $arguments) {
1229 $delegates = [
1230 $this->dbCore,
1231 $this->logsRepo,
1232 $this->redirectsRepo,
1233 $this->contentRepo,
1234 $this->statsRepo,
1235 $this->viewBuildOrchestrator,
1236 $this->viewReadService,
1237 ];
1238 foreach ($delegates as $delegate) {
1239 if ($delegate !== null && method_exists($delegate, $name)) {
1240 return $delegate->$name(...$arguments);
1241 }
1242 }
1243 throw new \BadMethodCallException(
1244 'Method ' . $name . '() not found on ' . static::class . ' or its sub-services.'
1245 );
1246 }
1247
1248 /** @param object $wpdb @param bool $allowReconnect @return bool */
1249 public function safeCheckConnection($wpdb, bool $allowReconnect = false): bool {
1250 return $this->dbCore->safeCheckConnection($wpdb, $allowReconnect);
1251 }
1252
1253 /** @return bool */
1254 public function ensureConnection() {
1255 return $this->dbCore->ensureConnection();
1256 }
1257
1258 /** @param string $query @return bool */
1259 public function queryStartsWithSelect(string $query): bool {
1260 return $this->dbCore->queryStartsWithSelect($query);
1261 }
1262
1263 /** @return string */
1264 public function stageFailurePolicy(): string {
1265 return 'database-core-classifier';
1266 }
1267
1268 /** @param string $query @return bool */
1269 public function queryProducesResultRows(string $query): bool {
1270 return $this->dbCore->queryProducesResultRows($query);
1271 }
1272
1273 /** @param string $query @param int $timeoutSeconds @return string */
1274 public function applyQueryTimeout(string $query, int $timeoutSeconds): string {
1275 return $this->dbCore->applyQueryTimeout($query, $timeoutSeconds);
1276 }
1277
1278 /** @return bool */
1279 public function isMariaDB(): bool {
1280 return $this->dbCore->isMariaDB();
1281 }
1282
1283 /** @param string $query @param int $timeoutSeconds @return string */
1284 public function applySelectTimeout(string $query, int $timeoutSeconds): string {
1285 return $this->dbCore->applySelectTimeout($query, $timeoutSeconds);
1286 }
1287
1288 /** @param string $query @param int $timeoutSeconds @return string */
1289 public function applyNonLeadingSelectTimeout(string $query, int $timeoutSeconds): string {
1290 return $this->dbCore->applyNonLeadingSelectTimeout($query, $timeoutSeconds);
1291 }
1292
1293 /** @param string $query @param int $timeoutSeconds @return string */
1294 public function applyStatementTimeout(string $query, int $timeoutSeconds): string {
1295 return $this->dbCore->applyStatementTimeout($query, $timeoutSeconds);
1296 }
1297
1298 /** @param string $insertSelectQuery @param int $timeoutSeconds @return string */
1299 public function applyTimeoutToInsertSelect(string $insertSelectQuery, int $timeoutSeconds): string {
1300 return $this->dbCore->applyTimeoutToInsertSelect($insertSelectQuery, $timeoutSeconds);
1301 }
1302
1303 /** @param string $query @return bool */
1304 public function queryHasSetStatementWrapper(string $query): bool {
1305 return $this->dbCore->queryHasSetStatementWrapper($query);
1306 }
1307
1308 /** @param string $query @return string */
1309 public function stripSetStatementWrapper(string $query): string {
1310 return $this->dbCore->stripSetStatementWrapper($query);
1311 }
1312
1313 /**
1314 * @param string $query
1315 * @param array<string, mixed> $result
1316 * @param 'OBJECT'|'OBJECT_K'|'ARRAY_A'|'ARRAY_N' $resultType
1317 * @return void
1318 */
1319 public function retryWithoutSetStatementWrapper(string &$query, array &$result, string $resultType): void {
1320 $this->dbCore->retryWithoutSetStatementWrapper($query, $result, $resultType);
1321 }
1322
1323 /**
1324 * @param ABJ_404_Solution_Clock $clock
1325 * @return void
1326 */
1327 public function setClock(ABJ_404_Solution_Clock $clock): void {
1328 $this->dbCore->setClock($clock);
1329 }
1330
1331 /**
1332 * Resolve the clock via DatabaseCore.
1333 *
1334 * @return ABJ_404_Solution_Clock
1335 */
1336 protected function clock(): ABJ_404_Solution_Clock {
1337 return $this->dbCore->clock();
1338 }
1339
1340 /** @return self */
1341 public static function getInstance() {
1342 if (self::$instance !== null) {
1343 return self::$instance;
1344 }
1345
1346 // If the DI container is initialized, prefer it.
1347 if (class_exists('ABJ_404_Solution_ServiceContainer')) {
1348 $resolved = ABJ_404_Solution_ServiceContainer::safeGet('data_access');
1349 if ($resolved instanceof self) {
1350 self::$instance = $resolved;
1351 return self::$instance;
1352 }
1353 }
1354
1355 // For backward compatibility, create with no arguments
1356 // The constructor will use getInstance() for dependencies
1357 self::$instance = new ABJ_404_Solution_DataAccess();
1358
1359 return self::$instance;
1360 }
1361
1362 /**
1363 * Check if a database table exists.
1364 *
1365 * Fix for missing table error (reported by 2 users - 4% of errors)
1366 * This prevents crashes when querying tables that don't exist or have
1367 * incorrect table prefixes, returning false instead of causing fatal errors.
1368 *
1369 * @param string $tableName Full table name to check (including prefix)
1370 * @return bool True if table exists, false otherwise
1371 */
1372 private function tableExists($tableName) {
1373 return $this->dbCore->tableExists($tableName);
1374 }
1375
1376 /**
1377 * Get the column names of an actual database table via SHOW COLUMNS.
1378 * Returns empty array on failure (table missing, permissions, etc.)
1379 * so callers can fall back to their default behavior.
1380 *
1381 * @param string $tableName Full table name (including prefix)
1382 * @return array<int, string>
1383 */
1384 private function getTableColumnNames(string $tableName): array {
1385 return $this->dbCore->getTableColumnNames($tableName);
1386 }
1387
1388 /** @return array{version: string, last_updated: string|null} */
1389 function getLatestPluginVersion() {
1390 // Cache version info to avoid repeated slow wordpress.org API calls.
1391 $cacheKey = 'abj404_latest_plugin_version_info';
1392 if (function_exists('get_transient')) {
1393 $cached = get_transient($cacheKey);
1394 if (is_array($cached) && isset($cached['version'])) {
1395 /** @var array{version: string, last_updated: string|null} $cached */
1396 return $cached;
1397 }
1398 }
1399
1400 if (!function_exists('plugins_api')) {
1401 require_once(ABSPATH . 'wp-admin/includes/plugin-install.php');
1402 }
1403 if (!function_exists('plugins_api')) {
1404 $this->logger->infoMessage("I couldn't find the plugins_api function to check for the latest version.");
1405 $fallback = array('version' => ABJ404_VERSION, 'last_updated' => null);
1406 return $fallback;
1407 }
1408
1409 $pluginSlug = dirname(ABJ404_NAME);
1410
1411 // set the arguments to get latest info from repository via API ##
1412 $args = array(
1413 'slug' => $pluginSlug,
1414 'fields' => array(
1415 'version' => true,
1416 'last_updated' => true,
1417 )
1418 );
1419
1420 /** Prepare our query */
1421 $call_api = plugins_api('plugin_information', $args);
1422
1423 /** Check for Errors & Display the results */
1424 if (is_wp_error($call_api)) {
1425 $api_error = $call_api->get_error_message();
1426 $this->logger->infoMessage("There was an API issue checking the latest plugin version ("
1427 . $api_error . ")");
1428
1429 $fallback = array('version' => ABJ404_VERSION, 'last_updated' => null);
1430 return $fallback;
1431 }
1432
1433 /** @var object $call_api */
1434 $apiVersion = property_exists($call_api, 'version') ? (string)$call_api->version : ABJ404_VERSION;
1435 $apiLastUpdated = property_exists($call_api, 'last_updated') ? (string)$call_api->last_updated : null;
1436 $result = array('version' => $apiVersion, 'last_updated' => $apiLastUpdated);
1437 if (function_exists('set_transient')) {
1438 $ttl = defined('DAY_IN_SECONDS') ? DAY_IN_SECONDS : 86400;
1439 // allow-cache-empty: $result always carries a version string (fallback to ABJ404_VERSION when plugins_api omits it); is_wp_error early-returns above
1440 set_transient($cacheKey, $result, $ttl);
1441 }
1442 return $result;
1443 }
1444
1445 /** Check wordpress.org for the latest version of this plugin. Return true if the latest version is installed,
1446 * false otherwise.
1447 * @return boolean
1448 */
1449 function shouldEmailErrorFile() {
1450 $abj404logging = abj_service('logging');
1451
1452 $pluginInfo = $this->getLatestPluginVersion();
1453
1454 $latestVersion = $pluginInfo['version'];
1455 $currentVersion = ABJ404_VERSION;
1456 if ($latestVersion == $currentVersion) {
1457 return true;
1458 }
1459
1460 if (version_compare(ABJ404_VERSION, $latestVersion) == 1) {
1461 $this->logger->infoMessage("Development version: A more recent version is installed than " .
1462 "what is available on the WordPress site (" . ABJ404_VERSION . " / " .
1463 $latestVersion . ").");
1464 return true;
1465 }
1466
1467 $currentArray = explode(".", $currentVersion);
1468 $latestArray = explode(".", $latestVersion);
1469
1470 // verify that the version numbers were parsed correctly.
1471 if (count($currentArray) != 3 || count($latestArray) != 3) {
1472 $this->logger->errorMessage("Issue parsing version numbers. " .
1473 $currentVersion . ' / ' . $latestVersion);
1474
1475 } else if ($currentArray[0] == $latestArray[0] && $currentArray[1] == $latestArray[1]) {
1476 // get the difference in the version numbers.
1477 $difference = absint(absint($latestArray[2]) - absint($currentArray[2]));
1478
1479 // if the major versions mostly match then send the error file.
1480 if ($difference <= 1) {
1481 return true;
1482 }
1483 }
1484
1485 return (ABJ404_VERSION == $pluginInfo['version']);
1486 }
1487
1488 /**
1489 * @return array<string, mixed>
1490 */
1491 function importDataFromPluginRedirectioner() {
1492 global $wpdb;
1493
1494 $oldTable = $wpdb->prefix . 'wbz404_redirects';
1495 $newTable = $this->dbCore->doTableNameReplacements('{wp_abj404_redirects}');
1496 // wp_wbz404_redirects -- old table
1497 // wp_abj404_redirects -- new table
1498
1499 $query = ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/sql/importDataFromPluginRedirectioner.sql");
1500 $query = $this->f->str_replace('{OLD_TABLE}', $oldTable, $query);
1501 $query = $this->f->str_replace('{NEW_TABLE}', $newTable, $query);
1502
1503 $result = $this->dbCore->queryAndGetResults($query);
1504
1505 $this->logger->infoMessage("Importing redirectioner SQL result: " .
1506 wp_kses_post((string)json_encode($result)));
1507
1508 return $result;
1509 }
1510
1511 }
1512