PluginProbe
404 Solution / trunk
404 Solution vtrunk
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 / stats / StatsDigestDataProvider.php

StatsDigestDataProvider.php in 404 Solution trunk, at includes/stats/StatsDigestDataProvider.php

159 lines 6.4 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 /**
8 * Provides digest-shaped stats rows consumed by email notifications.
9 */
10 class ABJ_404_Solution_StatsDigestDataProvider {
11
12 /** @var ABJ_404_Solution_DatabaseQueryInterface */
13 private $dbCore;
14 /** @var ABJ_404_Solution_DatabaseCollationHelper */
15 private $collationHelper;
16 /** @var ABJ_404_Solution_LogsRepositoryInterface */
17 private $logsRepo;
18 /** @var ABJ_404_Solution_StatsReadRepository */
19 private $statsReadRepository;
20 /** @var ABJ_404_Solution_Logging */
21 private $logger;
22
23 /**
24 * @param ABJ_404_Solution_DatabaseQueryInterface $dbCore
25 * @param ABJ_404_Solution_LogsRepositoryInterface $logsRepo
26 * @param ABJ_404_Solution_StatsReadRepository $statsReadRepository
27 * @param ABJ_404_Solution_Logging $logging
28 * @param ABJ_404_Solution_DatabaseCollationHelper $collationHelper
29 */
30 public function __construct(
31 ABJ_404_Solution_DatabaseQueryInterface $dbCore,
32 ABJ_404_Solution_LogsRepositoryInterface $logsRepo,
33 ABJ_404_Solution_StatsReadRepository $statsReadRepository,
34 $logging,
35 ABJ_404_Solution_DatabaseCollationHelper $collationHelper
36 ) {
37 $this->dbCore = $dbCore;
38 $this->logsRepo = $logsRepo;
39 $this->statsReadRepository = $statsReadRepository;
40 $this->logger = $logging;
41 $this->collationHelper = $collationHelper;
42 }
43
44 /**
45 * @param int $limit
46 * @return array<int, array<string, mixed>>
47 */
48 public function getTopCapturedForDigest(int $limit): array {
49 $limit = max(1, $limit);
50
51 if (!$this->logsRepo->logsHitsTableExists()) {
52 $this->logger->warn('getTopCapturedForDigest: logs_hits rollup unavailable; '
53 . 'digest top-captured table will be empty until rebuild completes. '
54 . 'EmailDigest pre-checks via logsHitsTableExists() to render an "unavailable" message instead.');
55 $this->logsRepo->scheduleHitsTableRebuild();
56 return array();
57 }
58
59 $query = $this->buildTopCapturedForDigestQuery($limit);
60 $result = $this->dbCore->queryAndGetResults($query, array('timeout' => 60));
61
62 if (!empty($result['timed_out']) || (isset($result['last_error']) && $result['last_error'] != '')) {
63 $errRaw = $result['last_error'] ?? '';
64 $errMsg = is_string($errRaw) ? $errRaw : '';
65 $timedOut = !empty($result['timed_out']);
66 $this->logger->warn('getTopCapturedForDigest: query failed against present rollup; '
67 . 'digest top-captured table will be empty. timed_out=' . ($timedOut ? '1' : '0')
68 . ', error=' . ($errMsg !== '' ? $errMsg : '(none)'));
69 return array();
70 }
71
72 $rawRows = is_array($result['rows'] ?? null) ? $result['rows'] : array();
73 $rows = array();
74 foreach ($rawRows as $row) {
75 if (is_array($row)) {
76 $rows[] = $row;
77 }
78 }
79 return $rows;
80 }
81
82 /** @param int $limit @return string */
83 public function buildTopCapturedForDigestQuery(int $limit): string {
84 $limit = max(1, $limit);
85 // Plain equality gives the optimizer an indexable requested_url probe;
86 // the BINARY predicate keeps exact-match URL semantics.
87 $logsHitsTable = $this->dbCore->doTableNameReplacements('{wp_abj404_logs_hits}');
88 $canonicalRedirectUrl = "COALESCE(r.canonical_url, CONCAT('/', TRIM(BOTH '/' FROM r.url)))";
89 $comparableRedirectUrl = $this->collationHelper->coerceExpressionToColumnCollation(
90 $canonicalRedirectUrl,
91 array('table' => $logsHitsTable, 'column' => 'requested_url')
92 );
93 // allow-unbounded-select: bounded by a runtime LIMIT (the query string is split across an ABJ404_STATUS_CAPTURED concatenation; the LIMIT $limit clause follows in the next literal)
94 $query = "SELECT r.url, COALESCE(h.logshits, 0) AS logshits, r.timestamp AS created
95 FROM {wp_abj404_redirects} r
96 LEFT JOIN {wp_abj404_logs_hits} h
97 ON h.requested_url = " . $comparableRedirectUrl . "
98 AND BINARY h.requested_url = BINARY
99 COALESCE(r.canonical_url, CONCAT('/', TRIM(BOTH '/' FROM r.url)))
100 WHERE r.status = " . ABJ404_STATUS_CAPTURED . " AND r.disabled = 0
101 ORDER BY logshits DESC, r.url ASC
102 LIMIT " . $limit;
103 return $this->dbCore->doTableNameReplacements($query);
104 }
105
106 /**
107 * @param callable|null $countProvider Optional facade count method for subclass compatibility.
108 * @return array{total_captured: int, total_manual: int, total_auto: int}
109 */
110 public function getDigestSummaryStats($countProvider = null): array {
111 $zero = array(
112 'total_captured' => 0,
113 'total_manual' => 0,
114 'total_auto' => 0,
115 );
116
117 $redirectsTable = $this->dbCore->doTableNameReplacements('{wp_abj404_redirects}');
118 $count = is_callable($countProvider)
119 ? $countProvider
120 : array($this->statsReadRepository, 'getStatsCount');
121
122 try {
123 $total_captured = call_user_func(
124 $count,
125 "SELECT COUNT(id) FROM {$redirectsTable} WHERE status = %d AND disabled = 0",
126 array(ABJ404_STATUS_CAPTURED)
127 );
128 $total_manual = call_user_func(
129 $count,
130 "SELECT COUNT(id) FROM {$redirectsTable} WHERE status = %d AND disabled = 0",
131 array(ABJ404_STATUS_MANUAL)
132 );
133 $total_auto = call_user_func(
134 $count,
135 "SELECT COUNT(id) FROM {$redirectsTable} WHERE status = %d AND disabled = 0",
136 array(ABJ404_STATUS_AUTO)
137 );
138 } catch (Throwable $e) {
139 $this->logger->warn(
140 'getRedirectsBreakdownStats failed; returning zero counts: '
141 . $e->getMessage()
142 );
143 return $zero;
144 }
145
146 return array(
147 'total_captured' => intval($total_captured),
148 'total_manual' => intval($total_manual),
149 'total_auto' => intval($total_auto),
150 );
151 }
152
153 /** @return int */
154 public function getCapturedCountForNotification(): int {
155 $viewRead = abj_service('view_read_service');
156 return $viewRead->getRecordCount(array(ABJ404_STATUS_CAPTURED));
157 }
158 }
159