PluginProbe
404 Solution / 4.3.0
404 Solution v4.3.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 / redirects / RedirectDeadDestinationScanner.php

RedirectDeadDestinationScanner.php in 404 Solution 4.3.0, at includes/redirects/RedirectDeadDestinationScanner.php

130 lines 4.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 require_once __DIR__ . '/RedirectDeadDestinationStore.php';
8
9 /**
10 * Flags redirects whose destination URL is present in the recent failed-hit rollup.
11 */
12 class ABJ_404_Solution_RedirectDeadDestinationScanner {
13
14 /** @var ABJ_404_Solution_DatabaseCore */
15 private $dbCore;
16
17 /** @var ABJ_404_Solution_Logging */
18 private $logger;
19
20 /** @var ABJ_404_Solution_RedirectDeadDestinationStore */
21 private $store;
22
23 /**
24 * @param ABJ_404_Solution_DatabaseCore $dbCore
25 * @param ABJ_404_Solution_Logging $logger
26 * @param ABJ_404_Solution_RedirectDeadDestinationStore|null $store
27 */
28 public function __construct(
29 ABJ_404_Solution_DatabaseCore $dbCore,
30 ABJ_404_Solution_Logging $logger,
31 ?ABJ_404_Solution_RedirectDeadDestinationStore $store = null
32 ) {
33 $this->dbCore = $dbCore;
34 $this->logger = $logger;
35 $this->store = $store !== null ? $store : new ABJ_404_Solution_RedirectDeadDestinationStore();
36 }
37
38 public function flagDeadDestinationRedirects(): void {
39 $flaggedIds = array();
40
41 $hitsTable = $this->dbCore->doTableNameReplacements('{wp_abj404_logs_hits}');
42 $hitsTableExists = $this->dbCore->tableNameResolver()->tableExists($hitsTable);
43
44 if (!$hitsTableExists || !$this->logsHitsHasFailedHitsColumn()) {
45 /** @var ABJ_404_Solution_LogsRepository|null $logsRepo */
46 $logsRepo = abj_service('logs_repository');
47 if ($logsRepo !== null) {
48 $logsRepo->scheduleHitsTableRebuild();
49 }
50 $this->store->storeIds($flaggedIds);
51 return;
52 }
53
54 // Plain equality gives the optimizer an indexable requested_url probe;
55 // the BINARY predicate keeps exact-match URL semantics.
56 $sql = "SELECT DISTINCT r.id
57 FROM {wp_abj404_redirects} r
58 INNER JOIN {wp_abj404_logs_hits} h
59 ON h.requested_url = CONCAT('/', TRIM(BOTH '/' FROM r.final_dest))
60 AND BINARY h.requested_url = BINARY CONCAT('/', TRIM(BOTH '/' FROM r.final_dest))
61 WHERE h.last_used > %d
62 AND h.failed_hits > 0
63 AND r.disabled = 0
64 AND r.final_dest != ''
65 AND r.final_dest != '0'";
66 $sql = $this->dbCore->doTableNameReplacements($sql);
67
68 $result = $this->dbCore->queryAndGetResults($sql, array(
69 'query_params' => array(abj_clock()->now() - 7 * 86400),
70 'timeout' => 30,
71 ));
72
73 if (empty($result['timed_out']) && (!isset($result['last_error']) || $result['last_error'] == '')) {
74 $rows = is_array($result['rows'] ?? null) ? $result['rows'] : array();
75 foreach ($rows as $row) {
76 $value = $this->extractIdValue($row);
77 if ($value !== null) {
78 $flaggedIds[] = $value;
79 }
80 }
81 }
82
83 $this->store->storeIds($flaggedIds);
84
85 if (!empty($flaggedIds)) {
86 $this->logger->infoMessage(
87 __CLASS__ . '/' . __FUNCTION__ . ': Flagged ' . count($flaggedIds) .
88 ' redirect(s) with dead destinations: ' . implode(', ', $flaggedIds)
89 );
90 }
91 }
92
93 /**
94 * @param mixed $row
95 * @return string|null
96 */
97 private function extractIdValue($row): ?string {
98 if (is_array($row)) {
99 $value = $row['id'] ?? reset($row);
100 } elseif (is_object($row)) {
101 $value = $row->id ?? null;
102 } else {
103 $value = $row;
104 }
105
106 if (is_scalar($value) && (string)$value !== '') {
107 return (string)$value;
108 }
109
110 return null;
111 }
112
113 private function logsHitsHasFailedHitsColumn(): bool {
114 $tableName = $this->dbCore->doTableNameReplacements('{wp_abj404_logs_hits}');
115 $sql = "SELECT 1 FROM information_schema.columns "
116 . "WHERE table_schema = DATABASE() "
117 . "AND table_name = %s "
118 . "AND column_name = 'failed_hits' LIMIT 1";
119 $result = $this->dbCore->queryAndGetResults($sql, array(
120 'query_params' => array($tableName),
121 'log_errors' => false,
122 ));
123 if (!empty($result['last_error'])) {
124 return false;
125 }
126 $rows = is_array($result['rows'] ?? null) ? $result['rows'] : array();
127 return !empty($rows);
128 }
129 }
130