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 / view / View_Tools.php

View_Tools.php in 404 Solution trunk, at includes/view/View_Tools.php

355 lines 17.3 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 // allow-no-test-found: covered through Tools page entry-point tests in tests/ViewToolsPageTest.php
8
9 /**
10 * Renders the Tools admin tab and debug-file viewer.
11 */
12 class ABJ_404_Solution_View_Tools extends ABJ_404_Solution_ViewComponent {
13
14 /**
15 * Load an HTML template from includes/html/ as a string.
16 *
17 * @param string $name Filename relative to includes/html/.
18 * @return string
19 */
20 private function tpl($name) {
21 return (string)ABJ_404_Solution_FileSystemService::readFileContents(dirname(__DIR__) . '/html/' . $name);
22 }
23
24 /** @return void */
25 function echoAdminDebugFile() {
26 $isPluginAdmin = abj_service('admin_access_policy')->isPluginAdmin();
27 if ($isPluginAdmin) {
28 $filesToEcho = array($this->logger->getDebugFilePath(),
29 $this->logger->getDebugFilePathOld());
30 $wrapperTpl = $this->tpl('viewStatsDebugFileWrapper.html');
31 for ($i = 0; $i < count($filesToEcho); $i++) {
32 $currentFile = $filesToEcho[$i];
33 ob_start();
34 $this->echoFileContents($currentFile);
35 $contents = (string)ob_get_clean();
36 $row = $this->f->str_replace('{file_name}', esc_html((string)$currentFile), $wrapperTpl);
37 $row = $this->f->str_replace('{contents}', $contents, $row);
38 echo $row;
39 }
40
41 } else {
42 echo "Non-admin request to view debug file.";
43 $current_user = ABJ_404_Solution_UserRef::fromWpUser(wp_get_current_user());
44 $userLogin = $current_user !== null ? $current_user->getLogin() : '';
45 $userDisplay = $current_user !== null ? $current_user->getDisplayName() : '';
46 $userEmail = $current_user !== null ? $current_user->getEmail() : '';
47 $userId = $current_user !== null ? $current_user->getId() : 0;
48 $userInfo = "Login: " . $userLogin . ", display name: " .
49 $userDisplay . ", Email: " . $userEmail .
50 ", UserID: " . $userId;
51 $this->logger->infoMessage("Non-admin request to view debug file. User info: " .
52 $userInfo);
53 }
54 }
55
56 /**
57 * @param string $fileName
58 * @return void
59 */
60 function echoFileContents($fileName) {
61 if (!is_string($fileName)) {
62 $fileName = '';
63 }
64
65 if (file_exists($fileName)) {
66 $linesRead = 0;
67 $handle = null;
68 try {
69 if ($handle = fopen($fileName, "r")) {
70 $truncatedTpl = $this->tpl('viewStatsDebugFileTruncated.html');
71 while (($line = fgets($handle)) !== false) {
72 $linesRead++;
73 echo nl2br(esc_html($line));
74
75 if ($linesRead > 1000000) {
76 echo $this->f->str_replace('{lines_read}', esc_html((string)$linesRead), $truncatedTpl);
77 break;
78 }
79 }
80 } else {
81 $this->logger->errorMessage("Error opening debug file.");
82 }
83
84 } catch (Exception $e) {
85 $this->logger->errorMessage("Error while reading debug file.", $e);
86 } finally {
87 // finally (not "after the try/catch") so the handle still
88 // closes if a Throwable that isn't an Exception escapes the
89 // loop -- the previous unconditional-looking cleanup below
90 // the try/catch was skipped in that case. Same
91 // resource-lifecycle shape as
92 // includes/import/ImportService.php::doImportFile().
93 if ($handle != null) {
94 fclose($handle);
95 }
96 }
97
98 } else {
99 echo nl2br(__('(The log file does not exist.)', '404-solution'));
100 }
101 }
102
103 /**
104 * Display the tools page.
105 * @return void
106 */
107 function echoAdminToolsPage() {
108 $view = $this->view;
109
110 $header = $this->tpl('viewStatsToolsPageHeader.html');
111 $header = $this->f->str_replace('{title}', esc_html__('Tools', '404-solution'), $header);
112 $header = $this->f->str_replace('{expand_all}', esc_html__('Expand All', '404-solution'), $header);
113 echo $header;
114
115 $link = wp_nonce_url("?page=" . ABJ404_PP . "&subpage=abj404_tools", "abj404_exportRedirects");
116 $html = ABJ_404_Solution_FileSystemService::readFileContents(dirname(__DIR__) . "/html/toolsExportForm.html");
117 $html = $this->f->str_replace('{toolsExportRedirectsLink}', $link, $html);
118 $html = $this->f->doNormalReplacements($html);
119 $view->echoOptionsSection(new ABJ_404_Solution_OptionsSectionView('tools-export', 'abj404-exportRedirects', __('Export', '404-solution'), $html, true, $view->getCardIcon('download')));
120
121 $link = wp_nonce_url("?page=" . ABJ404_PP . "&subpage=abj404_tools", "abj404_importRedirectsFile");
122 $html = ABJ_404_Solution_FileSystemService::readFileContents(dirname(__DIR__) . "/html/toolsImportForm.html");
123 $html = $this->f->str_replace('{toolsImportRedirectsLink}', $link, $html);
124 $html = $this->f->doNormalReplacements($html);
125 $view->echoOptionsSection(new ABJ_404_Solution_OptionsSectionView('tools-import', 'abj404-importRedirects', __('Import', '404-solution'), $html, false, $view->getCardIcon('upload')));
126
127 $url = "?page=" . ABJ404_PP . "&subpage=abj404_tools";
128 $link = wp_nonce_url($url, "abj404_purgeRedirects");
129 $html = ABJ_404_Solution_FileSystemService::readFileContents(dirname(__DIR__) . "/html/toolsPurgeForm.html");
130 $html = $this->f->str_replace('{toolsPurgeFormActionLink}', $link, $html);
131 $html = $this->f->doNormalReplacements($html);
132 $view->echoOptionsSection(new ABJ_404_Solution_OptionsSectionView('tools-purge', 'abj404-purgeRedirects', __('Purge Options', '404-solution'), $html, false, $view->getCardIcon('trash')));
133
134 $ngramLink = wp_nonce_url("?page=" . ABJ404_PP . "&subpage=abj404_tools", "abj404_rebuildNgramCache");
135 $spellingLink = wp_nonce_url("?page=" . ABJ404_PP . "&subpage=abj404_tools", "abj404_clearSpellingCache");
136 $html = ABJ_404_Solution_FileSystemService::readFileContents(dirname(__DIR__) . "/html/toolsCacheForm.html");
137 $html = $this->f->str_replace('{toolsNgramCacheFormActionLink}', $ngramLink, $html);
138 $html = $this->f->str_replace('{toolsSpellingCacheFormActionLink}', $spellingLink, $html);
139 $html = $this->f->doNormalReplacements($html);
140 $view->echoOptionsSection(new ABJ_404_Solution_OptionsSectionView('tools-cache', 'abj404-cacheTools', __('Cache Management', '404-solution'), $html, false, $view->getCardIcon('database')));
141
142 $html = $this->getToolsDiagnosticsMarkup();
143 $view->echoOptionsSection(new ABJ_404_Solution_OptionsSectionView('tools-diagnostics', 'abj404-diagnosticsTools', __('Diagnostics', '404-solution'), $html, false, $view->getCardIcon('warning')));
144
145 $link = wp_nonce_url("?page=" . ABJ404_PP . "&subpage=abj404_tools", "abj404_runMaintenance");
146 $link .= '&manually_fired=true';
147 $html = ABJ_404_Solution_FileSystemService::readFileContents(dirname(__DIR__) . "/html/toolsEtcForm.html");
148 $html = $this->f->str_replace('{toolsMaintenanceFormActionLink}', $link, $html);
149 $html = $this->f->doNormalReplacements($html);
150 $view->echoOptionsSection(new ABJ_404_Solution_OptionsSectionView('tools-etc', 'abj404-etcTools', __('Etcetera', '404-solution'), $html, false, $view->getCardIcon('cog')));
151
152 $html = $this->getMigrateFromPluginMarkup();
153 $view->echoOptionsSection(new ABJ_404_Solution_OptionsSectionView('tools-migrate', 'abj404-migrateFromPlugin', __('Migrate from Another Plugin', '404-solution'), $html, false, $view->getCardIcon('upload')));
154
155 echo $this->tpl('viewStatsToolsPageFooter.html');
156 }
157
158 /**
159 * Build the "Migrate from Another Plugin" card markup.
160 *
161 * @return string
162 */
163 public function getMigrateFromPluginMarkup(): string {
164 // Use the redirects repository already injected into this component
165 // (ViewComponent::$redirectsRepository) rather than re-resolving it from
166 // the service container. The dependency is in hand; reaching for
167 // abj_service() here would bypass the component's own wiring.
168 $redirectsRepo = is_object($this->redirectsRepository)
169 ? $this->redirectsRepository
170 : abj_service('redirects_repository');
171 $logger = abj_service('logging');
172 $importer = new ABJ_404_Solution_CrossPluginImporter($redirectsRepo, $logger);
173
174 $detected = $importer->detectInstalledPlugins();
175
176 $pluginLabels = array(
177 'rankmath' => __('Rank Math', '404-solution'),
178 'yoast' => __('Yoast SEO Premium', '404-solution'),
179 'aioseo' => __('AIOSEO', '404-solution'),
180 'safe-redirect-manager' => __('Safe Redirect Manager', '404-solution'),
181 'redirection' => __('Redirection Plugin', '404-solution'),
182 );
183
184 $availableSources = array();
185 foreach ($detected as $slug => $isAvailable) {
186 if ($isAvailable) {
187 $availableSources[$slug] = isset($pluginLabels[$slug]) ? $pluginLabels[$slug] : $slug;
188 }
189 }
190
191 $migrateActionUrl = wp_nonce_url(
192 '?page=' . ABJ404_PP . '&subpage=abj404_tools',
193 'abj404_importFromPlugin'
194 );
195
196 if (empty($availableSources)) {
197 $html = $this->tpl('viewStatsMigrateEmpty.html');
198 $html = $this->f->str_replace('{empty_text}', esc_html__('No supported redirect plugins detected on this site.', '404-solution'), $html);
199 $html = $this->f->str_replace('{supported_text}', esc_html__('Supported plugins: Rank Math, Yoast SEO Premium, AIOSEO, Safe Redirect Manager, Redirection.', '404-solution'), $html);
200 return $html;
201 }
202
203 $detectedNames = array_values($availableSources);
204
205 $previewNonce = wp_create_nonce('abj404_crossPluginPreview');
206 $ajaxUrl = admin_url('admin-ajax.php');
207
208 $optionTpl = $this->tpl('viewStatsMigrateOption.html');
209 $sourceOptionsHtml = '';
210 foreach ($availableSources as $slug => $label) {
211 $opt = $this->f->str_replace('{slug}', esc_attr((string)$slug), $optionTpl);
212 $opt = $this->f->str_replace('{label}', esc_html((string)$label), $opt);
213 $sourceOptionsHtml .= $opt;
214 }
215
216 // allow-em-dash: preserving shipped translation string verbatim (extracted, not authored, here)
217 $msgFound = __('Found %d redirect(s) from %s — proceed with import?', '404-solution');
218 // Encoded through ABJ_404_Solution_JsonResponseEncoder: every value below is a
219 // __() result, and a `gettext` filter returning bytes json_encode() cannot
220 // represent would otherwise make this false, then '' after the cast, then a
221 // JSON.parse('') throw in the importer's own script.
222 $migrateConfig = ABJ_404_Solution_JsonResponseEncoder::encode(array(
223 'ajaxUrl' => $ajaxUrl,
224 'nonce' => $previewNonce,
225 'msgFound' => $msgFound,
226 'msgNone' => __('No redirects found in %s. Nothing to import.', '404-solution'),
227 'msgError' => __('Could not fetch preview. Please try again.', '404-solution'),
228 ))->json();
229
230 $html = $this->tpl('viewStatsMigrateForm.html');
231 $html = $this->f->str_replace('{detected_label}', esc_html__('Detected redirect plugins:', '404-solution'), $html);
232 $html = $this->f->str_replace('{detected_names}', esc_html(implode(', ', $detectedNames)), $html);
233 $html = $this->f->str_replace('{source_label}', esc_html__('Source plugin:', '404-solution'), $html);
234 $html = $this->f->str_replace('{source_options}', $sourceOptionsHtml, $html);
235 $html = $this->f->str_replace('{preview_text}', esc_html__('Preview Import', '404-solution'), $html);
236 $html = $this->f->str_replace('{action_url}', esc_url($migrateActionUrl), $html);
237 $html = $this->f->str_replace('{confirm_text}', esc_attr__('Confirm Import', '404-solution'), $html);
238 $html = $this->f->str_replace('{back_text}', esc_html__('Back', '404-solution'), $html);
239 $html = $this->f->str_replace('{note_text}', esc_html__('This will import all active redirects from the selected plugin into 404 Solution. Regex and redirect codes are preserved.', '404-solution'), $html);
240 $html = $this->f->str_replace('{migrate_config}', esc_attr($migrateConfig), $html);
241
242 return $html;
243 }
244
245 /**
246 * Build compact diagnostics markup for the Tools page.
247 *
248 * @return string
249 */
250 public function getToolsDiagnosticsMarkup() {
251 $rows = $this->getToolsDiagnosticsRows();
252 $rowTpl = $this->tpl('viewStatsDiagnosticsRow.html');
253 $rowsHtml = '';
254
255 foreach ($rows as $row) {
256 $label = $this->readString($row, 'label');
257 $value = $this->readString($row, 'value');
258 $valueHtml = $this->readString($row, 'value_html');
259 $status = $this->readString($row, 'status');
260 if ($status === '') {
261 $status = 'info';
262 }
263 $statusLabel = ($status === 'ok') ? __('OK', '404-solution') : (($status === 'warn') ? __('Warning', '404-solution') : __('Info', '404-solution'));
264 $statusClass = ($status === 'ok') ? 'abj404-pill-success' : (($status === 'warn') ? 'abj404-pill-warning' : 'abj404-pill-info');
265
266 $valueCell = ($valueHtml !== '') ? wp_kses_post($valueHtml) : esc_html($value);
267
268 $rowHtml = $this->f->str_replace('{label}', esc_html($label), $rowTpl);
269 $rowHtml = $this->f->str_replace('{value_cell}', $valueCell, $rowHtml);
270 $rowHtml = $this->f->str_replace('{status_class}', esc_attr($statusClass), $rowHtml);
271 $rowHtml = $this->f->str_replace('{status_label}', esc_html($statusLabel), $rowHtml);
272 $rowsHtml .= $rowHtml;
273 }
274
275 $html = $this->tpl('viewStatsDiagnosticsTable.html');
276 $html = $this->f->str_replace('{intro_text}', esc_html__('Quick environment checks for troubleshooting and support.', '404-solution'), $html);
277 $html = $this->f->str_replace('{rows}', $rowsHtml, $html);
278
279 return $html;
280 }
281
282 /**
283 * Collect diagnostics fields displayed on the Tools page.
284 *
285 * @return array<int, array<string, mixed>>
286 */
287 public function getToolsDiagnosticsRows() {
288 $collector = new ABJ_404_Solution_ToolsDiagnostics();
289 $rows = $collector->getRows();
290 foreach ($rows as $index => $row) {
291 $controls = is_array($row) && isset($row['controls']) && is_array($row['controls'])
292 ? $row['controls']
293 : null;
294 if ($controls === null) {
295 continue;
296 }
297
298 $rows[$index]['value_html'] = $this->renderDiagnosticsControls($controls);
299 unset($rows[$index]['controls']);
300 }
301
302 return $rows;
303 }
304
305 /**
306 * @param array<mixed> $controls
307 * @return string
308 */
309 private function renderDiagnosticsControls(array $controls): string {
310 if (($controls['kind'] ?? '') !== 'simulated-db-latency') {
311 return '';
312 }
313
314 $urls = isset($controls['urls']) && is_array($controls['urls']) ? $controls['urls'] : array();
315 $labels = isset($controls['labels']) && is_array($controls['labels']) ? $controls['labels'] : array();
316
317 $controlsHtml = $this->tpl('viewStatsDiagnosticsLatencyControls.html');
318 $controlsHtml = $this->f->str_replace('{value_text}', esc_html($this->valueToString($controls['value_text'] ?? '')), $controlsHtml);
319 $controlsHtml = $this->f->str_replace('{url_250}', esc_url($this->valueToString($urls[250] ?? '')), $controlsHtml);
320 $controlsHtml = $this->f->str_replace('{url_500}', esc_url($this->valueToString($urls[500] ?? '')), $controlsHtml);
321 $controlsHtml = $this->f->str_replace('{url_900}', esc_url($this->valueToString($urls[900] ?? '')), $controlsHtml);
322 $controlsHtml = $this->f->str_replace('{url_0}', esc_url($this->valueToString($urls[0] ?? '')), $controlsHtml);
323 $controlsHtml = $this->f->str_replace('{label_250}', esc_html($this->valueToString($labels[250] ?? '')), $controlsHtml);
324 $controlsHtml = $this->f->str_replace('{label_500}', esc_html($this->valueToString($labels[500] ?? '')), $controlsHtml);
325 $controlsHtml = $this->f->str_replace('{label_900}', esc_html($this->valueToString($labels[900] ?? '')), $controlsHtml);
326 $controlsHtml = $this->f->str_replace('{label_disable}', esc_html($this->valueToString($labels[0] ?? '')), $controlsHtml);
327
328 return $controlsHtml;
329 }
330
331 /**
332 * @param array<mixed> $row
333 * @param string $key
334 * @return string
335 */
336 private function readString(array $row, string $key): string {
337 return $this->valueToString($row[$key] ?? '');
338 }
339
340 /**
341 * @param mixed $value
342 * @return string
343 */
344 private function valueToString($value): string {
345 if (is_string($value)) {
346 return $value;
347 }
348 if (is_int($value) || is_float($value) || is_bool($value)) {
349 return (string)$value;
350 }
351
352 return '';
353 }
354 }
355