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 / services / ImportCsvParser.php

ImportCsvParser.php in 404 Solution trunk, at includes/services/ImportCsvParser.php

233 lines 8.1 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 * Parses supported redirect-import CSV shapes into canonical row arrays.
9 */
10 class ABJ_404_Solution_ImportCsvParser {
11
12 /**
13 * @param mixed $line
14 * @return array<string, string>
15 */
16 function splitCsvLine($line) {
17 if (!is_string($line)) {
18 $line = is_scalar($line) ? (string)$line : '';
19 }
20
21 $data = array_map(function($v) {
22 return trim((string)$v);
23 }, str_getcsv($line, ',', '"', '\\'));
24
25 if (count($data) === 5) {
26 return array(
27 'from_url' => $data[0],
28 'status' => $data[1],
29 'type' => $data[2],
30 'to_url' => $data[3],
31 'wp_type' => $data[4]
32 );
33 } else if (count($data) === 2) {
34 return array(
35 'from_url' => $data[0],
36 'to_url' => $data[1]
37 );
38 }
39
40 return array('error' => sprintf(__('Invalid CSV format. %d columns found but 2 or 5 expected.', '404-solution'), count($data)));
41 }
42
43 /**
44 * @param array<int, string> $columns
45 * @return bool
46 */
47 function isCompatibleImportHeaderRow($columns) {
48 $normalized = $this->normalizeImportHeaders($columns);
49 $fromIndex = $this->findImportHeaderIndex($normalized, array('from_url', 'request', 'source', 'url', 'match_url'));
50 $toIndex = $this->findImportHeaderIndex($normalized, array('to_url', 'target', 'destination', 'action_data', 'redirect_to', 'url_to'));
51 return ($fromIndex !== -1 && $toIndex !== -1);
52 }
53
54 /**
55 * @param array<int, string> $columns
56 * @return array<int, string|null>
57 */
58 function normalizeImportHeaders($columns) {
59 return array_map(function($value) {
60 $value = preg_replace('/^\xEF\xBB\xBF/', '', (string)$value);
61 $value = trim(strtolower((string)$value));
62 return preg_replace('/[^a-z0-9_]/', '', str_replace(' ', '_', $value));
63 }, $columns);
64 }
65
66 /**
67 * @param array<int, string> $columns
68 * @return string
69 */
70 function detectImportFormatFromHeaders($columns) {
71 $normalized = $this->normalizeImportHeaders($columns);
72
73 if (in_array('source', $normalized, true) &&
74 in_array('target', $normalized, true) &&
75 in_array('regex', $normalized, true)) {
76 return 'redirection';
77 }
78
79 if (in_array('redirect_from', $normalized, true) &&
80 in_array('redirect_to', $normalized, true)) {
81 return 'safe_redirect_manager';
82 }
83
84 if (in_array('request', $normalized, true) &&
85 in_array('destination', $normalized, true)) {
86 return 'simple_301';
87 }
88
89 if ((in_array('from_url', $normalized, true) &&
90 in_array('to_url', $normalized, true)) ||
91 (in_array('from_url', $normalized, true) &&
92 in_array('status', $normalized, true) &&
93 in_array('type', $normalized, true) &&
94 in_array('to_url', $normalized, true))) {
95 return 'native';
96 }
97
98 return 'unknown';
99 }
100
101 /**
102 * @param array<int, string> $row
103 * @param array<int, string|null> $normalizedHeaders
104 * @return array<string, string>
105 */
106 function mapImportRowByHeaders($row, $normalizedHeaders) {
107 $fromIndex = $this->findImportHeaderIndex($normalizedHeaders, array('from_url', 'request', 'source', 'url', 'match_url'));
108 $toIndex = $this->findImportHeaderIndex($normalizedHeaders, array('to_url', 'target', 'destination', 'action_data', 'redirect_to', 'url_to'));
109
110 if ($fromIndex === -1 || $toIndex === -1) {
111 return array('error' => __('Invalid CSV format. Could not map source/destination columns.', '404-solution'));
112 }
113
114 $from = array_key_exists($fromIndex, $row) ? trim((string)$row[$fromIndex]) : '';
115 $to = array_key_exists($toIndex, $row) ? trim((string)$row[$toIndex]) : '';
116
117 if ($from === '' && $to === '') {
118 return array('from_url' => '', 'to_url' => '');
119 }
120
121 $result = array(
122 'from_url' => $from,
123 'to_url' => $to,
124 );
125
126 $engineIndex = $this->findImportHeaderIndex($normalizedHeaders, array('engine'));
127 if ($engineIndex !== -1 && array_key_exists($engineIndex, $row)) {
128 $result['engine'] = trim((string)$row[$engineIndex]);
129 }
130
131 $codeIndex = $this->findImportHeaderIndex($normalizedHeaders, array('code', 'redirect_code', 'http_code'));
132 if ($codeIndex !== -1 && array_key_exists($codeIndex, $row)) {
133 $result['code'] = trim((string)$row[$codeIndex]);
134 }
135
136 $statusIndex = $this->findImportHeaderIndex($normalizedHeaders, array('status', 'redirect_status'));
137 if ($statusIndex !== -1 && array_key_exists($statusIndex, $row)) {
138 $result['status'] = trim((string)$row[$statusIndex]);
139 }
140
141 $regexIndex = $this->findImportHeaderIndex($normalizedHeaders, array('regex', 'is_regex'));
142 if ($regexIndex !== -1 && array_key_exists($regexIndex, $row)) {
143 $result['regex'] = trim((string)$row[$regexIndex]);
144 }
145
146 return $result;
147 }
148
149 /**
150 * @param array<int, string|null> $headers
151 * @param array<int, string> $candidates
152 * @return int
153 */
154 private function findImportHeaderIndex($headers, $candidates) {
155 foreach ($candidates as $candidate) {
156 $idx = array_search($candidate, $headers, true);
157 if ($idx !== false) {
158 return (int)$idx;
159 }
160 }
161 return -1;
162 }
163
164 /**
165 * @param array<int, string> $columns
166 * @return array<string, string>
167 */
168 function mapImportRowWithoutHeaders($columns) {
169 $columns = array_values($columns);
170 if (count($columns) === 7) {
171 return array(
172 'from_url' => trim((string)$columns[0]),
173 'status' => trim((string)$columns[1]),
174 'type' => trim((string)$columns[2]),
175 'to_url' => trim((string)$columns[3]),
176 'wp_type' => trim((string)$columns[4]),
177 'engine' => trim((string)$columns[5]),
178 'code' => trim((string)$columns[6]),
179 );
180 }
181 if (count($columns) === 6) {
182 return array(
183 'from_url' => trim((string)$columns[0]),
184 'status' => trim((string)$columns[1]),
185 'type' => trim((string)$columns[2]),
186 'to_url' => trim((string)$columns[3]),
187 'wp_type' => trim((string)$columns[4]),
188 'engine' => trim((string)$columns[5]),
189 );
190 }
191 if (count($columns) === 5) {
192 return array(
193 'from_url' => trim((string)$columns[0]),
194 'status' => trim((string)$columns[1]),
195 'type' => trim((string)$columns[2]),
196 'to_url' => trim((string)$columns[3]),
197 'wp_type' => trim((string)$columns[4]),
198 );
199 }
200 if (count($columns) === 2) {
201 return array(
202 'from_url' => trim((string)$columns[0]),
203 'to_url' => trim((string)$columns[1]),
204 );
205 }
206 return array('error' => sprintf(__('Invalid CSV format. %d columns found but 2, 5, 6, or 7 expected.', '404-solution'), count($columns)));
207 }
208
209 /**
210 * @param resource $fileHandle
211 * @return string
212 */
213 function detectCsvDelimiterFromFile($fileHandle) {
214 while (($line = fgets($fileHandle)) !== false) {
215 if (trim($line) === '') {
216 continue;
217 }
218 $comma = count(str_getcsv($line, ',', '"', '\\'));
219 $semicolon = count(str_getcsv($line, ';', '"', '\\'));
220 $tab = count(str_getcsv($line, "\t", '"', '\\'));
221
222 if ($semicolon > $comma && $semicolon >= $tab) {
223 return ';';
224 }
225 if ($tab > $comma && $tab > $semicolon) {
226 return "\t";
227 }
228 return ',';
229 }
230 return ',';
231 }
232 }
233