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

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

187 lines 7.5 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 * Specialized, non-paginated reads of the redirects table for callers that
9 * don't go through the staged admin-list pipeline.
10 *
11 * Owns:
12 * - getRedirectsAll: id+url ordered listing (import preview, CSV browse)
13 * - doRedirectsExport: stream the redirects table to a CSV temp file
14 * - getRedirectsWithLogs: redirects joined with the logsv2 rollup
15 * - getRedirectsWithRegEx: regex redirects with a static request-scoped cache
16 * - getManualRedirectsWithRegexMetachars: manual redirects whose URL
17 * contains regex metacharacters (for the matcher's wildcard fallback)
18 * - getExtraDataToPermalinkSuggestions: post metadata for suggestion ids
19 *
20 * Extracted from ViewReadService in the i805 decomposition. The regex
21 * cache is still owned by RedirectsRepository (its static cache holds the
22 * per-request state); this reader just consults it.
23 */
24 class ABJ_404_Solution_RedirectsBulkReader {
25
26 /** @var ABJ_404_Solution_DatabaseCore */
27 private $dbCore;
28
29 /** @var ABJ_404_Solution_ViewQueryBuilder */
30 private $queryBuilder;
31
32 /** @var ABJ_404_Solution_Functions */
33 private $f;
34
35 /**
36 * @param ABJ_404_Solution_DatabaseCore $dbCore
37 * @param ABJ_404_Solution_ViewQueryBuilder $queryBuilder
38 * @param ABJ_404_Solution_Functions $f
39 */
40 public function __construct(
41 ABJ_404_Solution_DatabaseCore $dbCore,
42 ABJ_404_Solution_ViewQueryBuilder $queryBuilder,
43 $f
44 ) {
45 $this->dbCore = $dbCore;
46 $this->queryBuilder = $queryBuilder;
47 $this->f = $f;
48 }
49
50 /** @return array<int, array<string, mixed>> */
51 public function getRedirectsAll(): array {
52 $query = "select id, url from {wp_abj404_redirects} order by url";
53 $result = $this->dbCore->queryAndGetResults($query);
54 if (!empty($result['timed_out']) || (isset($result['last_error']) && $result['last_error'] != '')) {
55 return array();
56 }
57 $rows = is_array($result['rows'] ?? null) ? $result['rows'] : array();
58 return $rows;
59 }
60
61 /**
62 * Stream the export query straight to a CSV temp file via mysqli to keep
63 * the row buffer bounded on large redirect tables.
64 *
65 * @param string $tempFile
66 * @return void
67 */
68 public function doRedirectsExport(string $tempFile): void {
69 global $wpdb;
70
71 if (file_exists($tempFile)) {
72 ABJ_404_Solution_FileSystemService::safeUnlink($tempFile);
73 }
74
75 $query = ABJ_404_Solution_FileSystemService::readFileContents(__DIR__ . "/../sql/getRedirectsExport.sql");
76 $query = $this->dbCore->doTableNameReplacements($query);
77
78 $result = mysqli_query($wpdb->dbh, $query);
79 if ($result instanceof \mysqli_result) {
80 $fh = fopen($tempFile, 'w');
81 if ($fh === false) {
82 return;
83 }
84 fputcsv($fh, array('from_url', 'status', 'type', 'to_url', 'wp_type', 'engine', 'code'), ',', '"', '\\');
85
86 while (($row = mysqli_fetch_array($result, MYSQLI_ASSOC))) {
87 fputcsv($fh, array(
88 $row['from_url'],
89 $row['status'],
90 $row['type'],
91 $row['to_url'],
92 $row['type_wp'],
93 isset($row['engine']) ? $row['engine'] : '',
94 isset($row['code']) ? $row['code'] : '301'
95 ), ',', '"', '\\');
96 }
97 fclose($fh);
98 mysqli_free_result($result);
99 }
100 }
101
102 /** @return array<int, array<string, mixed>> */
103 public function getRedirectsWithLogs(): array {
104 $query = ABJ_404_Solution_FileSystemService::readFileContents(__DIR__ . "/../sql/getRedirectsWithLogs.sql");
105 $result = $this->dbCore->queryAndGetResults($query);
106 if (!empty($result['timed_out']) || (isset($result['last_error']) && $result['last_error'] != '')) {
107 return array();
108 }
109 $rows = is_array($result['rows'] ?? null) ? $result['rows'] : array();
110 return $rows;
111 }
112
113 /** @return array<int, array<string, mixed>> */
114 public function getRedirectsWithRegEx(): array {
115 $cached = ABJ_404_Solution_RedirectsRepository::getRegexRedirectsCache();
116 $disabled = ABJ_404_Solution_RedirectsRepository::isRegexCacheDisabled();
117
118 if ($cached !== null && !$disabled) {
119 return $cached;
120 }
121
122 if ($disabled) {
123 return $this->queryBuilder->queryRegexRedirects(ABJ_404_Solution_RedirectsRepository::REGEX_CACHE_MAX_COUNT + 1);
124 }
125
126 $results = $this->queryBuilder->queryRegexRedirects(ABJ_404_Solution_RedirectsRepository::REGEX_CACHE_MAX_COUNT + 1);
127
128 if (count($results) <= ABJ_404_Solution_RedirectsRepository::REGEX_CACHE_MAX_COUNT) {
129 ABJ_404_Solution_RedirectsRepository::setRegexRedirectsCache($results);
130 } else {
131 ABJ_404_Solution_RedirectsRepository::setRegexCacheDisabled(true);
132 }
133
134 return $results;
135 }
136
137 /** @return array<int, array<string, mixed>> */
138 // DESIGN-AUDIT-OK(2026-06-19, owner): status=MANUAL AND disabled=0 seeks via
139 // idx_status_disabled to the few hand-made MANUAL rows first, so INSTR() runs only
140 // over that bounded subset, not a full table scan. A LIMIT would drop valid manual
141 // regex redirects (changes results), so no cap. Reviewed + accepted 2026-06-18.
142 public function getManualRedirectsWithRegexMetachars(): array {
143 $query = "select \n {wp_abj404_redirects}.id,\n {wp_abj404_redirects}.url,\n {wp_abj404_redirects}.status,\n"
144 . " {wp_abj404_redirects}.type,\n {wp_abj404_redirects}.final_dest,\n {wp_abj404_redirects}.code,\n"
145 . " {wp_abj404_redirects}.timestamp,\n {wp_posts}.id as wp_post_id\n ";
146 $query .= "from {wp_abj404_redirects}\n " .
147 " LEFT OUTER JOIN {wp_posts} \n " .
148 " on {wp_abj404_redirects}.final_dest = {wp_posts}.id \n ";
149
150 $query .= "where status = " . ABJ404_STATUS_MANUAL . " \n " .
151 " and disabled = 0 \n " .
152 " and (INSTR(`url`, '*') > 0 " .
153 " OR INSTR(`url`, '[') > 0 " .
154 " OR INSTR(`url`, ']') > 0 " .
155 " OR INSTR(`url`, '|') > 0 " .
156 " OR INSTR(`url`, '^') > 0 " .
157 " OR INSTR(`url`, '\\\\') > 0 " .
158 " OR INSTR(`url`, '{') > 0 " .
159 " OR INSTR(`url`, '}') > 0)";
160 $results = $this->dbCore->queryAndGetResults($query);
161
162 /** @var array<int, array<string, mixed>> $rows */
163 $rows = is_array($results['rows']) ? $results['rows'] : array();
164 return $rows;
165 }
166
167 /**
168 * @param array<int, string> $postIDs
169 * @return array<int, mixed>
170 */
171 public function getExtraDataToPermalinkSuggestions(array $postIDs): array {
172 $postIDs = array_map('absint', $postIDs);
173 $postIDJoined = implode(', ', $postIDs);
174
175 $query = ABJ_404_Solution_FileSystemService::readFileContents(__DIR__ . "/../sql/getAdditionalPostData.sql");
176 $query = $this->f->str_replace('{IDS_TO_INCLUDE}', $postIDJoined, $query);
177 $query = $this->dbCore->doTableNameReplacements($query);
178 $query = $this->f->doNormalReplacements($query);
179
180 $results = $this->dbCore->queryAndGetResults($query);
181
182 /** @var array<int, mixed> $rows */
183 $rows = is_array($results['rows']) ? $results['rows'] : array();
184 return $rows;
185 }
186 }
187