PluginProbe
404 Solution / 4.3.3
404 Solution v4.3.3
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 4.3.3, at includes/view/View_Tools.php

351 lines 16.9 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 $migrateConfig = wp_json_encode(array(
219 'ajaxUrl' => $ajaxUrl,
220 'nonce' => $previewNonce,
221 'msgFound' => $msgFound,
222 'msgNone' => __('No redirects found in %s. Nothing to import.', '404-solution'),
223 'msgError' => __('Could not fetch preview. Please try again.', '404-solution'),
224 ));
225
226 $html = $this->tpl('viewStatsMigrateForm.html');
227 $html = $this->f->str_replace('{detected_label}', esc_html__('Detected redirect plugins:', '404-solution'), $html);
228 $html = $this->f->str_replace('{detected_names}', esc_html(implode(', ', $detectedNames)), $html);
229 $html = $this->f->str_replace('{source_label}', esc_html__('Source plugin:', '404-solution'), $html);
230 $html = $this->f->str_replace('{source_options}', $sourceOptionsHtml, $html);
231 $html = $this->f->str_replace('{preview_text}', esc_html__('Preview Import', '404-solution'), $html);
232 $html = $this->f->str_replace('{action_url}', esc_url($migrateActionUrl), $html);
233 $html = $this->f->str_replace('{confirm_text}', esc_attr__('Confirm Import', '404-solution'), $html);
234 $html = $this->f->str_replace('{back_text}', esc_html__('Back', '404-solution'), $html);
235 $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);
236 $html = $this->f->str_replace('{migrate_config}', esc_attr((string)$migrateConfig), $html);
237
238 return $html;
239 }
240
241 /**
242 * Build compact diagnostics markup for the Tools page.
243 *
244 * @return string
245 */
246 public function getToolsDiagnosticsMarkup() {
247 $rows = $this->getToolsDiagnosticsRows();
248 $rowTpl = $this->tpl('viewStatsDiagnosticsRow.html');
249 $rowsHtml = '';
250
251 foreach ($rows as $row) {
252 $label = $this->readString($row, 'label');
253 $value = $this->readString($row, 'value');
254 $valueHtml = $this->readString($row, 'value_html');
255 $status = $this->readString($row, 'status');
256 if ($status === '') {
257 $status = 'info';
258 }
259 $statusLabel = ($status === 'ok') ? __('OK', '404-solution') : (($status === 'warn') ? __('Warning', '404-solution') : __('Info', '404-solution'));
260 $statusClass = ($status === 'ok') ? 'abj404-pill-success' : (($status === 'warn') ? 'abj404-pill-warning' : 'abj404-pill-info');
261
262 $valueCell = ($valueHtml !== '') ? wp_kses_post($valueHtml) : esc_html($value);
263
264 $rowHtml = $this->f->str_replace('{label}', esc_html($label), $rowTpl);
265 $rowHtml = $this->f->str_replace('{value_cell}', $valueCell, $rowHtml);
266 $rowHtml = $this->f->str_replace('{status_class}', esc_attr($statusClass), $rowHtml);
267 $rowHtml = $this->f->str_replace('{status_label}', esc_html($statusLabel), $rowHtml);
268 $rowsHtml .= $rowHtml;
269 }
270
271 $html = $this->tpl('viewStatsDiagnosticsTable.html');
272 $html = $this->f->str_replace('{intro_text}', esc_html__('Quick environment checks for troubleshooting and support.', '404-solution'), $html);
273 $html = $this->f->str_replace('{rows}', $rowsHtml, $html);
274
275 return $html;
276 }
277
278 /**
279 * Collect diagnostics fields displayed on the Tools page.
280 *
281 * @return array<int, array<string, mixed>>
282 */
283 public function getToolsDiagnosticsRows() {
284 $collector = new ABJ_404_Solution_ToolsDiagnostics();
285 $rows = $collector->getRows();
286 foreach ($rows as $index => $row) {
287 $controls = is_array($row) && isset($row['controls']) && is_array($row['controls'])
288 ? $row['controls']
289 : null;
290 if ($controls === null) {
291 continue;
292 }
293
294 $rows[$index]['value_html'] = $this->renderDiagnosticsControls($controls);
295 unset($rows[$index]['controls']);
296 }
297
298 return $rows;
299 }
300
301 /**
302 * @param array<mixed> $controls
303 * @return string
304 */
305 private function renderDiagnosticsControls(array $controls): string {
306 if (($controls['kind'] ?? '') !== 'simulated-db-latency') {
307 return '';
308 }
309
310 $urls = isset($controls['urls']) && is_array($controls['urls']) ? $controls['urls'] : array();
311 $labels = isset($controls['labels']) && is_array($controls['labels']) ? $controls['labels'] : array();
312
313 $controlsHtml = $this->tpl('viewStatsDiagnosticsLatencyControls.html');
314 $controlsHtml = $this->f->str_replace('{value_text}', esc_html($this->valueToString($controls['value_text'] ?? '')), $controlsHtml);
315 $controlsHtml = $this->f->str_replace('{url_250}', esc_url($this->valueToString($urls[250] ?? '')), $controlsHtml);
316 $controlsHtml = $this->f->str_replace('{url_500}', esc_url($this->valueToString($urls[500] ?? '')), $controlsHtml);
317 $controlsHtml = $this->f->str_replace('{url_900}', esc_url($this->valueToString($urls[900] ?? '')), $controlsHtml);
318 $controlsHtml = $this->f->str_replace('{url_0}', esc_url($this->valueToString($urls[0] ?? '')), $controlsHtml);
319 $controlsHtml = $this->f->str_replace('{label_250}', esc_html($this->valueToString($labels[250] ?? '')), $controlsHtml);
320 $controlsHtml = $this->f->str_replace('{label_500}', esc_html($this->valueToString($labels[500] ?? '')), $controlsHtml);
321 $controlsHtml = $this->f->str_replace('{label_900}', esc_html($this->valueToString($labels[900] ?? '')), $controlsHtml);
322 $controlsHtml = $this->f->str_replace('{label_disable}', esc_html($this->valueToString($labels[0] ?? '')), $controlsHtml);
323
324 return $controlsHtml;
325 }
326
327 /**
328 * @param array<mixed> $row
329 * @param string $key
330 * @return string
331 */
332 private function readString(array $row, string $key): string {
333 return $this->valueToString($row[$key] ?? '');
334 }
335
336 /**
337 * @param mixed $value
338 * @return string
339 */
340 private function valueToString($value): string {
341 if (is_string($value)) {
342 return $value;
343 }
344 if (is_int($value) || is_float($value) || is_bool($value)) {
345 return (string)$value;
346 }
347
348 return '';
349 }
350 }
351