PluginProbe
404 Solution / 4.1.19
404 Solution v4.1.19
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 / DataAccessTrait_ViewQueriesStagedRead.php

DataAccessTrait_ViewQueriesStagedRead.php in 404 Solution 4.1.19, at includes/DataAccessTrait_ViewQueriesStagedRead.php

264 lines 11.8 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 * Read-side helpers for the staged getRedirectsForView pipeline.
9 *
10 * The build-side helpers in {@see ABJ_404_Solution_DataAccess_ViewQueriesStagedTrait}
11 * populate `{wp_abj404_view_done}`. This sibling trait is responsible for
12 * everything a request needs to fetch the page rows out of that table:
13 *
14 * readFromViewDone() / buildViewDoneReadQuery() / buildViewDoneCountQuery():
15 * translate the public $tableOptions filter/order/paging into SQL that
16 * runs against precomputed view_done columns (no JOINs, no CASE
17 * recomputation; legacy filter-text composite LIKE preserved).
18 *
19 * resolveStatusTypeList() / resolveOrderByColumn():
20 * bounded sanitisation of caller-supplied status filter and orderby
21 * column so the resulting SQL is safe to interpolate.
22 *
23 * viewBuildOnlyTranslations():
24 * labels that get baked into status_for_view / type_for_view at build
25 * time so the read path can serve them without round-tripping through
26 * __(); kept here because the labels logically describe view rendering.
27 *
28 * Composed alongside the build trait into ABJ_404_Solution_DataAccess so the
29 * cross-trait calls (queryAndGetResults, viewDoneTableName,
30 * stagedQueryOptions) resolve through the shared $this.
31 */
32 trait ABJ_404_Solution_DataAccess_ViewQueriesStagedReadTrait {
33
34 /**
35 * Read page from the served view_done table.
36 *
37 * @param string $sub
38 * @param array<string, mixed> $tableOptions
39 * @return array<int, array<string, mixed>>
40 */
41 private function readFromViewDone(string $sub, array $tableOptions): array {
42 $query = $this->buildViewDoneReadQuery($sub, $tableOptions);
43 $result = $this->queryAndGetResults($query, $this->stagedQueryOptions());
44 $rows = is_array($result['rows'] ?? null) ? $result['rows'] : array();
45 /** @var array<int, array<string, mixed>> $rows */
46 return $rows;
47 }
48
49 /**
50 * Build the WHERE/ORDER/LIMIT SELECT against view_done. Mirrors the
51 * legacy filter-text composite LIKE so search semantics are preserved,
52 * but reads against precomputed columns (no JOINs, no CASE
53 * recomputation).
54 *
55 * @param string $sub
56 * @param array<string, mixed> $tableOptions
57 * @return string
58 */
59 private function buildViewDoneReadQuery(string $sub, array $tableOptions): string {
60 global $abj404_redirect_types, $abj404_captured_types, $wpdb;
61
62 $statusTypes = $this->resolveStatusTypeList($sub, $tableOptions);
63 $trashValue = ($tableOptions['filter'] ?? 0) == ABJ404_TRASH_FILTER ? 1 : 0;
64 // Match legacy semantics: every tab including HANDLED filters by
65 // disabled = 0 (active rows) except the dedicated TRASH tab.
66 $trashClause = 'AND disabled = ' . intval($trashValue);
67
68 $rawScoreRange = $tableOptions['score_range'] ?? 'all';
69 $scoreRange = is_string($rawScoreRange) ? $rawScoreRange : 'all';
70 $scoreRangeClause = '';
71 switch ($scoreRange) {
72 case 'high': $scoreRangeClause = 'AND score >= 80'; break;
73 case 'medium': $scoreRangeClause = 'AND score >= 50 AND score < 80'; break;
74 case 'low': $scoreRangeClause = 'AND score IS NOT NULL AND score < 50'; break;
75 case 'manual': $scoreRangeClause = 'AND score IS NULL'; break;
76 }
77
78 $rawFilterText = $tableOptions['filterText'] ?? '';
79 $rawFilterText = is_string($rawFilterText) ? $rawFilterText : '';
80 $filterTextClause = '';
81 if ($rawFilterText !== '') {
82 $sanitized = str_replace(array('*', '/', '$'), '', $rawFilterText);
83 if (isset($wpdb) && method_exists($wpdb, 'esc_like')) {
84 /** @var \wpdb $wpdb */
85 $sanitized = $wpdb->esc_like($sanitized);
86 } else {
87 $sanitized = addcslashes($sanitized, '_%\\');
88 }
89 $filterText = esc_sql($sanitized);
90 if ($sub === 'abj404_redirects') {
91 $filterTextClause = "AND REPLACE(LOWER(CONCAT(url, '////', status_for_view, '////',"
92 . " type_for_view, '////', dest_for_view, '////', code)), ' ', '')"
93 . " LIKE REPLACE(LOWER('%" . $filterText . "%'), ' ', '')";
94 } else {
95 $filterTextClause = "AND REPLACE(LOWER(url), ' ', '')"
96 . " LIKE REPLACE(LOWER('%" . $filterText . "%'), ' ', '')";
97 }
98 }
99
100 $orderBy = $this->resolveOrderByColumn($tableOptions);
101 $rawOrderVal = $tableOptions['order'] ?? '';
102 $rawOrderValStr = is_string($rawOrderVal) ? $rawOrderVal : '';
103 $order = strtoupper((string)preg_replace('/[^a-zA-Z]/', '', trim($rawOrderValStr)));
104 if ($order !== 'DESC') { $order = 'ASC'; }
105
106 $rawPaged = $tableOptions['paged'] ?? 1;
107 $paged = max(1, is_scalar($rawPaged) ? intval($rawPaged) : 1);
108 $rawPerpage = $tableOptions['perpage'] ?? ABJ404_OPTION_DEFAULT_PERPAGE;
109 $perpage = max(1, is_scalar($rawPerpage) ? intval($rawPerpage) : (int)ABJ404_OPTION_DEFAULT_PERPAGE);
110 $limitStart = ($paged - 1) * $perpage;
111
112 $done = $this->viewDoneTableName();
113 $query = "SELECT id, url, status, status_for_view, type, type_for_view,\n"
114 . " final_dest, dest_for_view, published_status, code, timestamp,\n"
115 . " engine, score, wp_post_id, wp_post_type,\n"
116 . " logshits, logsid, last_used\n"
117 . "FROM `" . $done . "`\n"
118 . "WHERE status IN (" . $statusTypes . ")\n"
119 . " " . $trashClause . "\n"
120 . " " . $scoreRangeClause . "\n"
121 . " " . $filterTextClause . "\n"
122 . "ORDER BY published_status ASC, " . $orderBy . " " . $order . ", url ASC, id " . $order . "\n"
123 . "LIMIT " . $limitStart . ", " . $perpage;
124 return $query;
125 }
126
127 /**
128 * COUNT(*) variant of buildViewDoneReadQuery. Same WHERE clauses, no
129 * ORDER BY, no LIMIT.
130 *
131 * @param string $sub
132 * @param array<string, mixed> $tableOptions
133 * @return string
134 */
135 private function buildViewDoneCountQuery(string $sub, array $tableOptions): string {
136 global $wpdb;
137
138 $statusTypes = $this->resolveStatusTypeList($sub, $tableOptions);
139 $trashValue = ($tableOptions['filter'] ?? 0) == ABJ404_TRASH_FILTER ? 1 : 0;
140 // Match legacy semantics: HANDLED filter shows active rows only
141 // (disabled = 0), same as every non-TRASH tab.
142 $trashClause = 'AND disabled = ' . intval($trashValue);
143
144 $rawScoreRange = $tableOptions['score_range'] ?? 'all';
145 $scoreRange = is_string($rawScoreRange) ? $rawScoreRange : 'all';
146 $scoreRangeClause = '';
147 switch ($scoreRange) {
148 case 'high': $scoreRangeClause = 'AND score >= 80'; break;
149 case 'medium': $scoreRangeClause = 'AND score >= 50 AND score < 80'; break;
150 case 'low': $scoreRangeClause = 'AND score IS NOT NULL AND score < 50'; break;
151 case 'manual': $scoreRangeClause = 'AND score IS NULL'; break;
152 }
153
154 $rawFilterText = $tableOptions['filterText'] ?? '';
155 $rawFilterText = is_string($rawFilterText) ? $rawFilterText : '';
156 $filterTextClause = '';
157 if ($rawFilterText !== '') {
158 $sanitized = str_replace(array('*', '/', '$'), '', $rawFilterText);
159 if (isset($wpdb) && method_exists($wpdb, 'esc_like')) {
160 /** @var \wpdb $wpdb */
161 $sanitized = $wpdb->esc_like($sanitized);
162 } else {
163 $sanitized = addcslashes($sanitized, '_%\\');
164 }
165 $filterText = esc_sql($sanitized);
166 if ($sub === 'abj404_redirects') {
167 $filterTextClause = "AND REPLACE(LOWER(CONCAT(url, '////', status_for_view, '////',"
168 . " type_for_view, '////', dest_for_view, '////', code)), ' ', '')"
169 . " LIKE REPLACE(LOWER('%" . $filterText . "%'), ' ', '')";
170 } else {
171 $filterTextClause = "AND REPLACE(LOWER(url), ' ', '')"
172 . " LIKE REPLACE(LOWER('%" . $filterText . "%'), ' ', '')";
173 }
174 }
175
176 $done = $this->viewDoneTableName();
177 $query = "SELECT COUNT(*) AS cnt\n"
178 . "FROM `" . $done . "`\n"
179 . "WHERE status IN (" . $statusTypes . ")\n"
180 . " " . $trashClause . "\n"
181 . " " . $scoreRangeClause . "\n"
182 . " " . $filterTextClause;
183 return $query;
184 }
185
186 /**
187 * @param string $sub
188 * @param array<string, mixed> $tableOptions
189 * @return string
190 */
191 private function resolveStatusTypeList(string $sub, array $tableOptions): string {
192 global $abj404_redirect_types, $abj404_captured_types;
193 $filter = $tableOptions['filter'] ?? 0;
194 $statusTypes = '';
195 if ($filter == 0 || $filter == ABJ404_TRASH_FILTER) {
196 if ($sub === 'abj404_redirects') {
197 $types = array();
198 if (is_array($abj404_redirect_types)) {
199 foreach ($abj404_redirect_types as $t) {
200 $types[] = is_scalar($t) ? intval($t) : 0;
201 }
202 }
203 $statusTypes = implode(', ', $types);
204 } else if ($sub === 'abj404_captured') {
205 $types = array();
206 if (is_array($abj404_captured_types)) {
207 foreach ($abj404_captured_types as $t) {
208 $types[] = is_scalar($t) ? intval($t) : 0;
209 }
210 }
211 $statusTypes = implode(', ', $types);
212 }
213 } else if ($filter == ABJ404_STATUS_MANUAL) {
214 $statusTypes = implode(', ', array(ABJ404_STATUS_MANUAL, ABJ404_STATUS_REGEX));
215 } else if ($filter == ABJ404_HANDLED_FILTER) {
216 $statusTypes = implode(', ', array(ABJ404_STATUS_IGNORED, ABJ404_STATUS_LATER));
217 } else {
218 $statusTypes = is_scalar($filter) ? (string)$filter : '';
219 }
220 $cleaned = preg_replace('/[^\d, ]/', '', $statusTypes);
221 return is_string($cleaned) ? $cleaned : '';
222 }
223
224 /**
225 * @param array<string, mixed> $tableOptions
226 * @return string
227 */
228 private function resolveOrderByColumn(array $tableOptions): string {
229 $rawOrderBy = $tableOptions['orderby'] ?? '';
230 $orderBy = strtolower(is_string($rawOrderBy) ? $rawOrderBy : '');
231 $allowed = array('url', 'status', 'type', 'code', 'score', 'timestamp',
232 'logshits', 'last_used', 'final_dest', 'dest', 'id');
233 if ($orderBy === 'dest' || $orderBy === 'final_dest') {
234 // Same as legacy: treat empty dest as last.
235 return "CASE WHEN dest_for_view IS NULL OR dest_for_view = '' THEN 1 ELSE 0 END ASC, dest_for_view";
236 }
237 if (!in_array($orderBy, $allowed, true)) {
238 $orderBy = 'url';
239 }
240 return $orderBy;
241 }
242
243 /**
244 * Translations for status_for_view, type_for_view, and the special
245 * 404-displayed label. Everything else (`{wp_*}`, `{ABJ404_TYPE_X}`)
246 * is handled by doTableNameReplacements + doNormalReplacements.
247 *
248 * @return array<string, string>
249 */
250 private function viewBuildOnlyTranslations(): array {
251 return array(
252 '{ABJ404_STATUS_MANUAL_text}' => __('Manual', '404-solution'),
253 '{ABJ404_STATUS_AUTO_text}' => __('Automatic', '404-solution'),
254 '{ABJ404_STATUS_REGEX_text}' => __('Regex', '404-solution'),
255 '{ABJ404_TYPE_EXTERNAL_text}' => __('External', '404-solution'),
256 '{ABJ404_TYPE_CAT_text}' => __('Category', '404-solution'),
257 '{ABJ404_TYPE_TAG_text}' => __('Tag', '404-solution'),
258 '{ABJ404_TYPE_HOME_text}' => __('Home', '404-solution'),
259 '{ABJ404_TYPE_404_DISPLAYED_text}' => __('(404 page)', '404-solution'),
260 '{ABJ404_TYPE_SPECIAL_text}' => __('Special', '404-solution'),
261 );
262 }
263 }
264