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

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

553 lines 24.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 * SQL query construction and execution for admin list views.
9 *
10 * Builds the SQL for redirect/captured list queries, view_done reads,
11 * status type resolution, order-by mapping, and translation maps.
12 */
13 class ABJ_404_Solution_ViewQueryBuilder {
14
15 /** @var ABJ_404_Solution_DatabaseCore */
16 private $dbCore;
17
18 /** @var ABJ_404_Solution_Functions */
19 private $f;
20
21 /** @var ABJ_404_Solution_LogsRepository */
22 private $logsRepo;
23
24 /** @var ABJ_404_Solution_Logging */
25 private $logger;
26
27 /** @var ABJ_404_Solution_ViewReadServiceInterface|null */
28 private $host;
29
30 /** @var ABJ_404_Solution_ViewBuildOrchestratorInterface|null */
31 private $viewBuildOrchestrator;
32
33 /**
34 * @param ABJ_404_Solution_DatabaseCore $dbCore
35 * @param ABJ_404_Solution_Functions $f
36 * @param ABJ_404_Solution_LogsRepository $logsRepo
37 * @param ABJ_404_Solution_Logging $logger
38 */
39 public function __construct(
40 ABJ_404_Solution_DatabaseCore $dbCore,
41 ABJ_404_Solution_Functions $f,
42 ABJ_404_Solution_LogsRepository $logsRepo,
43 $logger
44 ) {
45 $this->dbCore = $dbCore;
46 $this->f = $f;
47 $this->logsRepo = $logsRepo;
48 $this->logger = $logger;
49 }
50
51 /**
52 * @param ABJ_404_Solution_ViewReadServiceInterface $host
53 * @return void
54 */
55 public function setHost(ABJ_404_Solution_ViewReadServiceInterface $host): void {
56 $this->host = $host;
57 }
58
59 /**
60 * @param ABJ_404_Solution_ViewBuildOrchestratorInterface $viewBuildOrchestrator
61 * @return void
62 */
63 public function setViewBuildOrchestrator(ABJ_404_Solution_ViewBuildOrchestratorInterface $viewBuildOrchestrator): void {
64 $this->viewBuildOrchestrator = $viewBuildOrchestrator;
65 }
66
67 /** @return ABJ_404_Solution_ViewBuildOrchestratorInterface */
68 private function requireViewBuildOrchestrator(): ABJ_404_Solution_ViewBuildOrchestratorInterface {
69 if ($this->viewBuildOrchestrator === null) {
70 throw new \RuntimeException('ViewQueryBuilder requires ViewBuildOrchestrator (call setViewBuildOrchestrator first)'); // allow-raw-error: assertion, should never reach user
71 }
72 return $this->viewBuildOrchestrator;
73 }
74
75 /** @return string */
76 private function viewDoneTableName(): string {
77 return $this->dbCore->doTableNameReplacements('{wp_abj404_view_done}');
78 }
79
80 /**
81 * @return string Fully-replaced SQL (table-name placeholders resolved).
82 */
83 public function buildHighImpactCapturedCountQuery(): string {
84 $query = "SELECT COUNT(*) AS cnt
85 FROM {wp_abj404_redirects} r
86 INNER JOIN {wp_abj404_logs_hits} h
87 ON BINARY h.requested_url = BINARY
88 COALESCE(r.canonical_url, CONCAT('/', TRIM(BOTH '/' FROM r.url)))
89 WHERE r.status = " . ABJ404_STATUS_CAPTURED . " AND r.disabled = 0
90 AND h.logshits >= 3";
91 return $this->dbCore->doTableNameReplacements($query);
92 }
93
94 /**
95 * @return array<int, array<string, mixed>>
96 */
97 public function queryRegexRedirects() {
98 $query = "select \n {wp_abj404_redirects}.id,\n {wp_abj404_redirects}.url,\n {wp_abj404_redirects}.status,\n"
99 . " {wp_abj404_redirects}.type,\n {wp_abj404_redirects}.final_dest,\n {wp_abj404_redirects}.code,\n"
100 . " {wp_abj404_redirects}.timestamp,\n {wp_posts}.id as wp_post_id\n ";
101 $query .= "from {wp_abj404_redirects}\n " .
102 " LEFT OUTER JOIN {wp_posts} \n " .
103 " on {wp_abj404_redirects}.final_dest = {wp_posts}.id \n ";
104
105 $query .= "where status in (" . ABJ404_STATUS_REGEX . ") \n " .
106 " and disabled = 0";
107 $results = $this->dbCore->queryAndGetResults($query);
108
109 /** @var array<int, array<string, mixed>> $rows */
110 $rows = is_array($results['rows']) ? $results['rows'] : array();
111 return $rows;
112 }
113
114 /**
115 * @param string $sub
116 * @param array<string, mixed> $tableOptions
117 * @return string
118 */
119 public function getOptimizedRedirectsForViewCountQuery(string $sub, array $tableOptions): string {
120 global $abj404_redirect_types, $abj404_captured_types;
121
122 $statusTypes = '';
123 if ($tableOptions['filter'] == 0 || $tableOptions['filter'] == ABJ404_TRASH_FILTER) {
124 if ($sub == 'abj404_redirects') {
125 $statusTypes = implode(", ", $abj404_redirect_types);
126 } else if ($sub == 'abj404_captured') {
127 $statusTypes = implode(", ", $abj404_captured_types);
128 }
129 } else if ($tableOptions['filter'] == ABJ404_STATUS_MANUAL) {
130 $statusTypes = implode(", ", array(ABJ404_STATUS_MANUAL, ABJ404_STATUS_REGEX));
131 } else if ($tableOptions['filter'] == ABJ404_HANDLED_FILTER) {
132 $statusTypes = implode(", ", array(ABJ404_STATUS_IGNORED, ABJ404_STATUS_LATER));
133 } else {
134 $statusTypes = $tableOptions['filter'];
135 }
136 $statusTypes = preg_replace('/[^\d, ]/', '', trim(is_string($statusTypes) ? $statusTypes : ''));
137
138 $trashValue = ($tableOptions['filter'] == ABJ404_TRASH_FILTER) ? 1 : 0;
139
140 $scoreRangeClause = '';
141 $rawScoreRange = is_string($tableOptions['score_range'] ?? '') ? ($tableOptions['score_range'] ?? 'all') : 'all';
142 switch ($rawScoreRange) {
143 case 'high': $scoreRangeClause = 'AND wp_abj404_redirects.score >= 80'; break; // allow-prefix-literal: SQL alias, see comment above
144 case 'medium': $scoreRangeClause = 'AND wp_abj404_redirects.score >= 50 AND wp_abj404_redirects.score < 80'; break; // allow-prefix-literal: SQL alias
145 case 'low': $scoreRangeClause = 'AND wp_abj404_redirects.score IS NOT NULL AND wp_abj404_redirects.score < 50'; break; // allow-prefix-literal: SQL alias
146 case 'manual': $scoreRangeClause = 'AND wp_abj404_redirects.score IS NULL'; break; // allow-prefix-literal: SQL alias
147 }
148
149 $query = "SELECT COUNT(*) AS count\n" .
150 "FROM {wp_abj404_redirects} wp_abj404_redirects\n" . // allow-prefix-literal: second token is the SQL alias name, not a table reference
151 "WHERE 1 and status IN (" . $statusTypes . ") AND disabled = " . intval($trashValue) . "\n" .
152 $scoreRangeClause;
153
154 return $this->dbCore->doTableNameReplacements($query);
155 }
156
157 /**
158 * @param string $sub
159 * @param array<string, mixed> $tableOptions
160 * @param bool $queryAllRowsAtOnce
161 * @param int $limitStart
162 * @param int $limitEnd
163 * @param bool $selectCountOnly
164 * @return string
165 */
166 public function getRedirectsForViewQuery($sub, $tableOptions, $queryAllRowsAtOnce,
167 $limitStart, $limitEnd, $selectCountOnly) {
168 global $abj404_redirect_types;
169 global $abj404_captured_types;
170 global $wpdb;
171
172 $logsTableColumns = '';
173 $logsTableColumns = "null as logshits, \n null as logsid, \n null as last_used, \n";
174 $logsTableJoin = '';
175 $statusTypes = '';
176 $trashValue = '';
177 $selectCountReplacement = '/* selecting data as usual */';
178
179 if ($selectCountOnly) {
180 $selectCountReplacement = "\n /*+ SET_VAR(max_join_size=18446744073709551615) */\n" .
181 "count(*) as count\n /* only selecting for count";
182 }
183
184 if ($queryAllRowsAtOnce && !$selectCountOnly) {
185 if ($this->host !== null) {
186 $this->host->maybeUpdateRedirectsForViewHitsTable();
187 }
188
189 if ($this->logsRepo->logsHitsTableExists()) {
190 $logsTableColumns = "logstable.logshits as logshits, \n" .
191 "logstable.logsid, \n" .
192 "logstable.last_used, \n";
193
194 $logsTableJoin = " LEFT OUTER JOIN {wp_abj404_logs_hits} logstable \n " .
195 " on binary logstable.requested_url = " .
196 "binary COALESCE(wp_abj404_redirects.canonical_url, " . // allow-prefix-literal: SQL alias
197 "concat('/', trim(both '/' from wp_abj404_redirects.url))) \n "; // allow-prefix-literal: SQL alias
198 } else {
199 $this->logger->debugMessage("logs_hits table not available, falling back to null columns");
200 }
201 }
202
203 if ($tableOptions['filter'] == 0 || $tableOptions['filter'] == ABJ404_TRASH_FILTER) {
204 if ($sub == 'abj404_redirects') {
205 $statusTypes = implode(", ", $abj404_redirect_types);
206
207 } else if ($sub == 'abj404_captured') {
208 $statusTypes = implode(", ", $abj404_captured_types);
209
210 } else {
211 $this->logger->errorMessage("Unrecognized sub type: " . esc_html($sub));
212 }
213
214 } else if ($tableOptions['filter'] == ABJ404_STATUS_MANUAL) {
215 $statusTypes = implode(", ", array(ABJ404_STATUS_MANUAL, ABJ404_STATUS_REGEX));
216
217 } else if ($tableOptions['filter'] == ABJ404_HANDLED_FILTER) {
218 $statusTypes = implode(", ", array(ABJ404_STATUS_IGNORED, ABJ404_STATUS_LATER));
219
220 } else {
221 $statusTypes = $tableOptions['filter'];
222 }
223 $statusTypes = preg_replace('/[^\d, ]/', '', trim(is_string($statusTypes) ? $statusTypes : ''));
224
225 if ($tableOptions['filter'] == ABJ404_TRASH_FILTER) {
226 $trashValue = 1;
227 } else if ($tableOptions['filter'] == ABJ404_HANDLED_FILTER) {
228 $trashValue = 0;
229 } else {
230 $trashValue = 0;
231 }
232
233 $orderByString = '';
234 if (!$selectCountOnly) {
235 $rawOrderBy = $tableOptions['orderby'] ?? '';
236 $orderBy = $this->f->strtolower(is_string($rawOrderBy) ? $rawOrderBy : '');
237 if ($orderBy == "final_dest") {
238 // TODO change the final dest type to an integer and store external URLs somewhere else.
239 $orderBy = "case when post_title is null then 1 else 0 end asc, post_title";
240 } else {
241 $orderBy = preg_replace('/[^a-zA-Z_]/', '', trim($orderBy));
242 }
243 $rawOrderVal = $tableOptions['order'] ?? '';
244 $rawOrderValX = is_string($rawOrderVal) ? $rawOrderVal : '';
245 $order = strtoupper((string)preg_replace('/[^a-zA-Z_]/', '', trim($rawOrderValX)));
246 if ($order !== 'DESC') {
247 $order = 'ASC';
248 }
249 $orderByString = "order by published_status asc, " . $orderBy . " " . $order .
250 ", wp_abj404_redirects.url ASC, wp_abj404_redirects.id " . $order; // allow-prefix-literal: SQL alias bound by `FROM {wp_abj404_redirects} wp_abj404_redirects`
251 }
252
253 $rawScoreRange = is_string($tableOptions['score_range'] ?? '') ? ($tableOptions['score_range'] ?? 'all') : 'all';
254 switch ($rawScoreRange) {
255 case 'high':
256 $scoreRangeClause = 'AND wp_abj404_redirects.score >= 80'; // allow-prefix-literal: SQL alias
257 break;
258 case 'medium':
259 $scoreRangeClause = 'AND wp_abj404_redirects.score >= 50 AND wp_abj404_redirects.score < 80'; // allow-prefix-literal: SQL alias
260 break;
261 case 'low':
262 $scoreRangeClause = 'AND wp_abj404_redirects.score IS NOT NULL AND wp_abj404_redirects.score < 50'; // allow-prefix-literal: SQL alias
263 break;
264 case 'manual':
265 $scoreRangeClause = 'AND wp_abj404_redirects.score IS NULL'; // allow-prefix-literal: SQL alias
266 break;
267 default:
268 $scoreRangeClause = '';
269 break;
270 }
271
272 $searchFilterForRedirectsExists = "no redirects fiter text found";
273 $searchFilterForCapturedExists = "no captured 404s filter text found";
274 $filterText = '';
275 $rawFilterText = is_string($tableOptions['filterText'] ?? null) ? $tableOptions['filterText'] : '';
276 if ($rawFilterText != '') {
277 if ($sub == 'abj404_redirects') {
278 $searchFilterForRedirectsExists = ' filter text enabled */';
279
280 } else if ($sub == 'abj404_captured') {
281 $searchFilterForCapturedExists = ' filter text enabled */';
282
283 } else {
284 throw new Exception("Unrecognized page for filter text request."); // allow-raw-error: legacy assertion, pre-existing code moved from ViewReadService.php
285 }
286 }
287
288 $filterTextRaw = str_replace(array('*', '/', '$'), '', $rawFilterText);
289 if (isset($wpdb) && is_object($wpdb) && method_exists($wpdb, 'esc_like')) {
290 /** @var wpdb $wpdb */
291 $filterTextRaw = $wpdb->esc_like($filterTextRaw);
292 } else {
293 $filterTextRaw = addcslashes($filterTextRaw, '_%\\');
294 }
295 $filterText = esc_sql($filterTextRaw);
296
297 $query = ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/sql/getRedirectsForView.sql");
298 $wpdbCollate = 'utf8mb4_unicode_ci';
299 $hasForcedCollate = false;
300 if (array_key_exists('forceCollate', $tableOptions) && !empty($tableOptions['forceCollate'])) {
301 $rawForceCollateVal = $tableOptions['forceCollate'];
302 $rawForceCollate = is_string($rawForceCollateVal) ? $rawForceCollateVal : '';
303 $forced = preg_replace('/[^A-Za-z0-9_]/', '', $rawForceCollate);
304 if ($forced !== '') {
305 $wpdbCollate = $forced;
306 $hasForcedCollate = true;
307 }
308 }
309 if (!$hasForcedCollate && isset($wpdb) && isset($wpdb->collate) && !empty($wpdb->collate)) {
310 $wpdbCollate = preg_replace('/[^A-Za-z0-9_]/', '', $wpdb->collate);
311 }
312 if ($wpdbCollate === '') {
313 $wpdbCollate = 'utf8mb4_unicode_ci';
314 }
315 $query = $this->f->str_replace('{selecting-for-count-true-false}', $selectCountReplacement, $query);
316 $query = $this->f->str_replace('{statusTypes}', $statusTypes, $query);
317 $query = $this->f->str_replace('{orderByString}', $orderByString, $query);
318 $query = $this->f->str_replace('{limitStart}', (string)$limitStart, $query);
319 $query = $this->f->str_replace('{limitEnd}', (string)$limitEnd, $query);
320 $query = $this->f->str_replace('{searchFilterForRedirectsExists}', $searchFilterForRedirectsExists, $query);
321 $query = $this->f->str_replace('{searchFilterForCapturedExists}', $searchFilterForCapturedExists, $query);
322 $query = $this->f->str_replace('{filterText}', $filterText, $query);
323 $query = $this->f->str_replace('{wpdb_collate}', $wpdbCollate, $query);
324 $query = $this->f->str_replace('{logsTableColumns}', $logsTableColumns, $query);
325 $query = $this->f->str_replace('{logsTableJoin}', $logsTableJoin, $query);
326 $query = $this->f->str_replace('{trashValue}', (string)$trashValue, $query);
327 $query = $this->f->str_replace('{scoreRangeClause}', $scoreRangeClause, $query);
328 $query = $this->dbCore->doTableNameReplacements($query);
329
330 if (array_key_exists('translations', $tableOptions) && is_array($tableOptions['translations'])) {
331 $keys = array_keys($tableOptions['translations']);
332 $values = array_values($tableOptions['translations']);
333 /** @var array<int, string> $keys */
334 $query = $this->f->str_replace($keys, array_map('strval', $values), $query);
335 }
336
337 $query = $this->f->doNormalReplacements($query);
338
339 return $query;
340 }
341
342 /**
343 * @param string $sub
344 * @param array<string, mixed> $tableOptions
345 * @return array<int, array<string, mixed>>
346 */
347 public function readFromViewDone(string $sub, array $tableOptions): array {
348 $query = $this->buildViewDoneReadQuery($sub, $tableOptions);
349 $result = $this->dbCore->queryAndGetResults($query, $this->requireViewBuildOrchestrator()->getStagedQueryOptionsForRead());
350 $rows = is_array($result['rows'] ?? null) ? $result['rows'] : array();
351 /** @var array<int, array<string, mixed>> $rows */
352 return $rows;
353 }
354
355 /**
356 * @param string $sub
357 * @param array<string, mixed> $tableOptions
358 * @return string
359 */
360 private function buildViewDoneReadQuery(string $sub, array $tableOptions): string {
361 global $abj404_redirect_types, $abj404_captured_types, $wpdb;
362
363 $statusTypes = $this->resolveStatusTypeList($sub, $tableOptions);
364 $trashValue = ($tableOptions['filter'] ?? 0) == ABJ404_TRASH_FILTER ? 1 : 0;
365 $trashClause = 'AND disabled = ' . intval($trashValue);
366
367 $rawScoreRange = $tableOptions['score_range'] ?? 'all';
368 $scoreRange = is_string($rawScoreRange) ? $rawScoreRange : 'all';
369 $scoreRangeClause = '';
370 switch ($scoreRange) {
371 case 'high': $scoreRangeClause = 'AND score >= 80'; break;
372 case 'medium': $scoreRangeClause = 'AND score >= 50 AND score < 80'; break;
373 case 'low': $scoreRangeClause = 'AND score IS NOT NULL AND score < 50'; break;
374 case 'manual': $scoreRangeClause = 'AND score IS NULL'; break;
375 }
376
377 $rawFilterText = $tableOptions['filterText'] ?? '';
378 $rawFilterText = is_string($rawFilterText) ? $rawFilterText : '';
379 $filterTextClause = '';
380 if ($rawFilterText !== '') {
381 $sanitized = str_replace(array('*', '/', '$'), '', $rawFilterText);
382 if (isset($wpdb) && method_exists($wpdb, 'esc_like')) {
383 /** @var \wpdb $wpdb */
384 $sanitized = $wpdb->esc_like($sanitized);
385 } else {
386 $sanitized = addcslashes($sanitized, '_%\\');
387 }
388 $filterText = esc_sql($sanitized);
389 if ($sub === 'abj404_redirects') {
390 $filterTextClause = "AND REPLACE(LOWER(CONCAT(url, '////', status_for_view, '////',"
391 . " type_for_view, '////', dest_for_view, '////', code)), ' ', '')"
392 . " LIKE REPLACE(LOWER('%" . $filterText . "%'), ' ', '')";
393 } else {
394 $filterTextClause = "AND REPLACE(LOWER(url), ' ', '')"
395 . " LIKE REPLACE(LOWER('%" . $filterText . "%'), ' ', '')";
396 }
397 }
398
399 $orderBy = $this->resolveOrderByColumn($tableOptions);
400 $rawOrderVal = $tableOptions['order'] ?? '';
401 $rawOrderValStr = is_string($rawOrderVal) ? $rawOrderVal : '';
402 $order = strtoupper((string)preg_replace('/[^a-zA-Z]/', '', trim($rawOrderValStr)));
403 if ($order !== 'DESC') { $order = 'ASC'; }
404
405 $rawPaged = $tableOptions['paged'] ?? 1;
406 $paged = max(1, is_scalar($rawPaged) ? intval($rawPaged) : 1);
407 $rawPerpage = $tableOptions['perpage'] ?? ABJ404_OPTION_DEFAULT_PERPAGE;
408 $perpage = max(1, is_scalar($rawPerpage) ? intval($rawPerpage) : (int)ABJ404_OPTION_DEFAULT_PERPAGE);
409 $limitStart = ($paged - 1) * $perpage;
410
411 $done = $this->viewDoneTableName();
412 $query = "SELECT id, url, status, status_for_view, type, type_for_view,\n"
413 . " final_dest, dest_for_view, published_status, code, timestamp,\n"
414 . " engine, score, wp_post_id, wp_post_type,\n"
415 . " logshits, logsid, last_used\n"
416 . "FROM `" . $done . "`\n"
417 . "WHERE status IN (" . $statusTypes . ")\n"
418 . " " . $trashClause . "\n"
419 . " " . $scoreRangeClause . "\n"
420 . " " . $filterTextClause . "\n"
421 . "ORDER BY published_status ASC, " . $orderBy . " " . $order . ", url ASC, id " . $order . "\n"
422 . "LIMIT " . $limitStart . ", " . $perpage;
423 return $query;
424 }
425
426 /**
427 * @param string $sub
428 * @param array<string, mixed> $tableOptions
429 * @return string
430 */
431 public function buildViewDoneCountQuery(string $sub, array $tableOptions): string {
432 global $wpdb;
433
434 $statusTypes = $this->resolveStatusTypeList($sub, $tableOptions);
435 $trashValue = ($tableOptions['filter'] ?? 0) == ABJ404_TRASH_FILTER ? 1 : 0;
436 $trashClause = 'AND disabled = ' . intval($trashValue);
437
438 $rawScoreRange = $tableOptions['score_range'] ?? 'all';
439 $scoreRange = is_string($rawScoreRange) ? $rawScoreRange : 'all';
440 $scoreRangeClause = '';
441 switch ($scoreRange) {
442 case 'high': $scoreRangeClause = 'AND score >= 80'; break;
443 case 'medium': $scoreRangeClause = 'AND score >= 50 AND score < 80'; break;
444 case 'low': $scoreRangeClause = 'AND score IS NOT NULL AND score < 50'; break;
445 case 'manual': $scoreRangeClause = 'AND score IS NULL'; break;
446 }
447
448 $rawFilterText = $tableOptions['filterText'] ?? '';
449 $rawFilterText = is_string($rawFilterText) ? $rawFilterText : '';
450 $filterTextClause = '';
451 if ($rawFilterText !== '') {
452 $sanitized = str_replace(array('*', '/', '$'), '', $rawFilterText);
453 if (isset($wpdb) && method_exists($wpdb, 'esc_like')) {
454 /** @var \wpdb $wpdb */
455 $sanitized = $wpdb->esc_like($sanitized);
456 } else {
457 $sanitized = addcslashes($sanitized, '_%\\');
458 }
459 $filterText = esc_sql($sanitized);
460 if ($sub === 'abj404_redirects') {
461 $filterTextClause = "AND REPLACE(LOWER(CONCAT(url, '////', status_for_view, '////',"
462 . " type_for_view, '////', dest_for_view, '////', code)), ' ', '')"
463 . " LIKE REPLACE(LOWER('%" . $filterText . "%'), ' ', '')";
464 } else {
465 $filterTextClause = "AND REPLACE(LOWER(url), ' ', '')"
466 . " LIKE REPLACE(LOWER('%" . $filterText . "%'), ' ', '')";
467 }
468 }
469
470 $done = $this->viewDoneTableName();
471 $query = "SELECT COUNT(*) AS cnt\n"
472 . "FROM `" . $done . "`\n"
473 . "WHERE status IN (" . $statusTypes . ")\n"
474 . " " . $trashClause . "\n"
475 . " " . $scoreRangeClause . "\n"
476 . " " . $filterTextClause;
477 return $query;
478 }
479
480 /**
481 * @param string $sub
482 * @param array<string, mixed> $tableOptions
483 * @return string
484 */
485 public function resolveStatusTypeList(string $sub, array $tableOptions): string {
486 global $abj404_redirect_types, $abj404_captured_types;
487 $filter = $tableOptions['filter'] ?? 0;
488 $statusTypes = '';
489 if ($filter == 0 || $filter == ABJ404_TRASH_FILTER) {
490 if ($sub === 'abj404_redirects') {
491 $types = array();
492 if (is_array($abj404_redirect_types)) {
493 foreach ($abj404_redirect_types as $t) {
494 $types[] = is_scalar($t) ? intval($t) : 0;
495 }
496 }
497 $statusTypes = implode(', ', $types);
498 } else if ($sub === 'abj404_captured') {
499 $types = array();
500 if (is_array($abj404_captured_types)) {
501 foreach ($abj404_captured_types as $t) {
502 $types[] = is_scalar($t) ? intval($t) : 0;
503 }
504 }
505 $statusTypes = implode(', ', $types);
506 }
507 } else if ($filter == ABJ404_STATUS_MANUAL) {
508 $statusTypes = implode(', ', array(ABJ404_STATUS_MANUAL, ABJ404_STATUS_REGEX));
509 } else if ($filter == ABJ404_HANDLED_FILTER) {
510 $statusTypes = implode(', ', array(ABJ404_STATUS_IGNORED, ABJ404_STATUS_LATER));
511 } else {
512 $statusTypes = is_scalar($filter) ? (string)$filter : '';
513 }
514 $cleaned = preg_replace('/[^\d, ]/', '', $statusTypes);
515 return is_string($cleaned) ? $cleaned : '';
516 }
517
518 /**
519 * @param array<string, mixed> $tableOptions
520 * @return string
521 */
522 public function resolveOrderByColumn(array $tableOptions): string {
523 $rawOrderBy = $tableOptions['orderby'] ?? '';
524 $orderBy = strtolower(is_string($rawOrderBy) ? $rawOrderBy : '');
525 $allowed = array('url', 'status', 'type', 'code', 'score', 'timestamp',
526 'logshits', 'last_used', 'final_dest', 'dest', 'id');
527 if ($orderBy === 'dest' || $orderBy === 'final_dest') {
528 return "CASE WHEN dest_for_view IS NULL OR dest_for_view = '' THEN 1 ELSE 0 END ASC, dest_for_view";
529 }
530 if (!in_array($orderBy, $allowed, true)) {
531 $orderBy = 'url';
532 }
533 return $orderBy;
534 }
535
536 /**
537 * @return array<string, string>
538 */
539 public function viewBuildOnlyTranslations(): array {
540 return array(
541 '{ABJ404_STATUS_MANUAL_text}' => __('Manual', '404-solution'),
542 '{ABJ404_STATUS_AUTO_text}' => __('Automatic', '404-solution'),
543 '{ABJ404_STATUS_REGEX_text}' => __('Regex', '404-solution'),
544 '{ABJ404_TYPE_EXTERNAL_text}' => __('External', '404-solution'),
545 '{ABJ404_TYPE_CAT_text}' => __('Category', '404-solution'),
546 '{ABJ404_TYPE_TAG_text}' => __('Tag', '404-solution'),
547 '{ABJ404_TYPE_HOME_text}' => __('Home', '404-solution'),
548 '{ABJ404_TYPE_404_DISPLAYED_text}' => __('(404 page)', '404-solution'),
549 '{ABJ404_TYPE_SPECIAL_text}' => __('Special', '404-solution'),
550 );
551 }
552 }
553