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

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

345 lines 16.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 // 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 }
87
88 if ($handle != null) {
89 fclose($handle);
90 }
91
92 } else {
93 echo nl2br(__('(The log file does not exist.)', '404-solution'));
94 }
95 }
96
97 /**
98 * Display the tools page.
99 * @return void
100 */
101 function echoAdminToolsPage() {
102 $view = $this->view;
103
104 $header = $this->tpl('viewStatsToolsPageHeader.html');
105 $header = $this->f->str_replace('{title}', esc_html__('Tools', '404-solution'), $header);
106 $header = $this->f->str_replace('{expand_all}', esc_html__('Expand All', '404-solution'), $header);
107 echo $header;
108
109 $link = wp_nonce_url("?page=" . ABJ404_PP . "&subpage=abj404_tools", "abj404_exportRedirects");
110 $html = ABJ_404_Solution_FileSystemService::readFileContents(dirname(__DIR__) . "/html/toolsExportForm.html");
111 $html = $this->f->str_replace('{toolsExportRedirectsLink}', $link, $html);
112 $html = $this->f->doNormalReplacements($html);
113 $view->echoOptionsSection(new ABJ_404_Solution_OptionsSectionView('tools-export', 'abj404-exportRedirects', __('Export', '404-solution'), $html, true, $view->getCardIcon('download')));
114
115 $link = wp_nonce_url("?page=" . ABJ404_PP . "&subpage=abj404_tools", "abj404_importRedirectsFile");
116 $html = ABJ_404_Solution_FileSystemService::readFileContents(dirname(__DIR__) . "/html/toolsImportForm.html");
117 $html = $this->f->str_replace('{toolsImportRedirectsLink}', $link, $html);
118 $html = $this->f->doNormalReplacements($html);
119 $view->echoOptionsSection(new ABJ_404_Solution_OptionsSectionView('tools-import', 'abj404-importRedirects', __('Import', '404-solution'), $html, false, $view->getCardIcon('upload')));
120
121 $url = "?page=" . ABJ404_PP . "&subpage=abj404_tools";
122 $link = wp_nonce_url($url, "abj404_purgeRedirects");
123 $html = ABJ_404_Solution_FileSystemService::readFileContents(dirname(__DIR__) . "/html/toolsPurgeForm.html");
124 $html = $this->f->str_replace('{toolsPurgeFormActionLink}', $link, $html);
125 $html = $this->f->doNormalReplacements($html);
126 $view->echoOptionsSection(new ABJ_404_Solution_OptionsSectionView('tools-purge', 'abj404-purgeRedirects', __('Purge Options', '404-solution'), $html, false, $view->getCardIcon('trash')));
127
128 $ngramLink = wp_nonce_url("?page=" . ABJ404_PP . "&subpage=abj404_tools", "abj404_rebuildNgramCache");
129 $spellingLink = wp_nonce_url("?page=" . ABJ404_PP . "&subpage=abj404_tools", "abj404_clearSpellingCache");
130 $html = ABJ_404_Solution_FileSystemService::readFileContents(dirname(__DIR__) . "/html/toolsCacheForm.html");
131 $html = $this->f->str_replace('{toolsNgramCacheFormActionLink}', $ngramLink, $html);
132 $html = $this->f->str_replace('{toolsSpellingCacheFormActionLink}', $spellingLink, $html);
133 $html = $this->f->doNormalReplacements($html);
134 $view->echoOptionsSection(new ABJ_404_Solution_OptionsSectionView('tools-cache', 'abj404-cacheTools', __('Cache Management', '404-solution'), $html, false, $view->getCardIcon('database')));
135
136 $html = $this->getToolsDiagnosticsMarkup();
137 $view->echoOptionsSection(new ABJ_404_Solution_OptionsSectionView('tools-diagnostics', 'abj404-diagnosticsTools', __('Diagnostics', '404-solution'), $html, false, $view->getCardIcon('warning')));
138
139 $link = wp_nonce_url("?page=" . ABJ404_PP . "&subpage=abj404_tools", "abj404_runMaintenance");
140 $link .= '&manually_fired=true';
141 $html = ABJ_404_Solution_FileSystemService::readFileContents(dirname(__DIR__) . "/html/toolsEtcForm.html");
142 $html = $this->f->str_replace('{toolsMaintenanceFormActionLink}', $link, $html);
143 $html = $this->f->doNormalReplacements($html);
144 $view->echoOptionsSection(new ABJ_404_Solution_OptionsSectionView('tools-etc', 'abj404-etcTools', __('Etcetera', '404-solution'), $html, false, $view->getCardIcon('cog')));
145
146 $html = $this->getMigrateFromPluginMarkup();
147 $view->echoOptionsSection(new ABJ_404_Solution_OptionsSectionView('tools-migrate', 'abj404-migrateFromPlugin', __('Migrate from Another Plugin', '404-solution'), $html, false, $view->getCardIcon('upload')));
148
149 echo $this->tpl('viewStatsToolsPageFooter.html');
150 }
151
152 /**
153 * Build the "Migrate from Another Plugin" card markup.
154 *
155 * @return string
156 */
157 public function getMigrateFromPluginMarkup(): string {
158 // Use the redirects repository already injected into this component
159 // (ViewComponent::$redirectsRepository) rather than re-resolving it from
160 // the service container. The dependency is in hand; reaching for
161 // abj_service() here would bypass the component's own wiring.
162 $redirectsRepo = is_object($this->redirectsRepository)
163 ? $this->redirectsRepository
164 : abj_service('redirects_repository');
165 $logger = abj_service('logging');
166 $importer = new ABJ_404_Solution_CrossPluginImporter($redirectsRepo, $logger);
167
168 $detected = $importer->detectInstalledPlugins();
169
170 $pluginLabels = array(
171 'rankmath' => __('Rank Math', '404-solution'),
172 'yoast' => __('Yoast SEO Premium', '404-solution'),
173 'aioseo' => __('AIOSEO', '404-solution'),
174 'safe-redirect-manager' => __('Safe Redirect Manager', '404-solution'),
175 'redirection' => __('Redirection Plugin', '404-solution'),
176 );
177
178 $availableSources = array();
179 foreach ($detected as $slug => $isAvailable) {
180 if ($isAvailable) {
181 $availableSources[$slug] = isset($pluginLabels[$slug]) ? $pluginLabels[$slug] : $slug;
182 }
183 }
184
185 $migrateActionUrl = wp_nonce_url(
186 '?page=' . ABJ404_PP . '&subpage=abj404_tools',
187 'abj404_importFromPlugin'
188 );
189
190 if (empty($availableSources)) {
191 $html = $this->tpl('viewStatsMigrateEmpty.html');
192 $html = $this->f->str_replace('{empty_text}', esc_html__('No supported redirect plugins detected on this site.', '404-solution'), $html);
193 $html = $this->f->str_replace('{supported_text}', esc_html__('Supported plugins: Rank Math, Yoast SEO Premium, AIOSEO, Safe Redirect Manager, Redirection.', '404-solution'), $html);
194 return $html;
195 }
196
197 $detectedNames = array_values($availableSources);
198
199 $previewNonce = wp_create_nonce('abj404_crossPluginPreview');
200 $ajaxUrl = admin_url('admin-ajax.php');
201
202 $optionTpl = $this->tpl('viewStatsMigrateOption.html');
203 $sourceOptionsHtml = '';
204 foreach ($availableSources as $slug => $label) {
205 $opt = $this->f->str_replace('{slug}', esc_attr((string)$slug), $optionTpl);
206 $opt = $this->f->str_replace('{label}', esc_html((string)$label), $opt);
207 $sourceOptionsHtml .= $opt;
208 }
209
210 // allow-em-dash: preserving shipped translation string verbatim (extracted, not authored, here)
211 $msgFound = __('Found %d redirect(s) from %s — proceed with import?', '404-solution');
212 $migrateConfig = wp_json_encode(array(
213 'ajaxUrl' => $ajaxUrl,
214 'nonce' => $previewNonce,
215 'msgFound' => $msgFound,
216 'msgNone' => __('No redirects found in %s. Nothing to import.', '404-solution'),
217 'msgError' => __('Could not fetch preview. Please try again.', '404-solution'),
218 ));
219
220 $html = $this->tpl('viewStatsMigrateForm.html');
221 $html = $this->f->str_replace('{detected_label}', esc_html__('Detected redirect plugins:', '404-solution'), $html);
222 $html = $this->f->str_replace('{detected_names}', esc_html(implode(', ', $detectedNames)), $html);
223 $html = $this->f->str_replace('{source_label}', esc_html__('Source plugin:', '404-solution'), $html);
224 $html = $this->f->str_replace('{source_options}', $sourceOptionsHtml, $html);
225 $html = $this->f->str_replace('{preview_text}', esc_html__('Preview Import', '404-solution'), $html);
226 $html = $this->f->str_replace('{action_url}', esc_url($migrateActionUrl), $html);
227 $html = $this->f->str_replace('{confirm_text}', esc_attr__('Confirm Import', '404-solution'), $html);
228 $html = $this->f->str_replace('{back_text}', esc_html__('Back', '404-solution'), $html);
229 $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);
230 $html = $this->f->str_replace('{migrate_config}', esc_attr((string)$migrateConfig), $html);
231
232 return $html;
233 }
234
235 /**
236 * Build compact diagnostics markup for the Tools page.
237 *
238 * @return string
239 */
240 public function getToolsDiagnosticsMarkup() {
241 $rows = $this->getToolsDiagnosticsRows();
242 $rowTpl = $this->tpl('viewStatsDiagnosticsRow.html');
243 $rowsHtml = '';
244
245 foreach ($rows as $row) {
246 $label = $this->readString($row, 'label');
247 $value = $this->readString($row, 'value');
248 $valueHtml = $this->readString($row, 'value_html');
249 $status = $this->readString($row, 'status');
250 if ($status === '') {
251 $status = 'info';
252 }
253 $statusLabel = ($status === 'ok') ? __('OK', '404-solution') : (($status === 'warn') ? __('Warning', '404-solution') : __('Info', '404-solution'));
254 $statusClass = ($status === 'ok') ? 'abj404-pill-success' : (($status === 'warn') ? 'abj404-pill-warning' : 'abj404-pill-info');
255
256 $valueCell = ($valueHtml !== '') ? wp_kses_post($valueHtml) : esc_html($value);
257
258 $rowHtml = $this->f->str_replace('{label}', esc_html($label), $rowTpl);
259 $rowHtml = $this->f->str_replace('{value_cell}', $valueCell, $rowHtml);
260 $rowHtml = $this->f->str_replace('{status_class}', esc_attr($statusClass), $rowHtml);
261 $rowHtml = $this->f->str_replace('{status_label}', esc_html($statusLabel), $rowHtml);
262 $rowsHtml .= $rowHtml;
263 }
264
265 $html = $this->tpl('viewStatsDiagnosticsTable.html');
266 $html = $this->f->str_replace('{intro_text}', esc_html__('Quick environment checks for troubleshooting and support.', '404-solution'), $html);
267 $html = $this->f->str_replace('{rows}', $rowsHtml, $html);
268
269 return $html;
270 }
271
272 /**
273 * Collect diagnostics fields displayed on the Tools page.
274 *
275 * @return array<int, array<string, mixed>>
276 */
277 public function getToolsDiagnosticsRows() {
278 $collector = new ABJ_404_Solution_ToolsDiagnostics();
279 $rows = $collector->getRows();
280 foreach ($rows as $index => $row) {
281 $controls = is_array($row) && isset($row['controls']) && is_array($row['controls'])
282 ? $row['controls']
283 : null;
284 if ($controls === null) {
285 continue;
286 }
287
288 $rows[$index]['value_html'] = $this->renderDiagnosticsControls($controls);
289 unset($rows[$index]['controls']);
290 }
291
292 return $rows;
293 }
294
295 /**
296 * @param array<mixed> $controls
297 * @return string
298 */
299 private function renderDiagnosticsControls(array $controls): string {
300 if (($controls['kind'] ?? '') !== 'simulated-db-latency') {
301 return '';
302 }
303
304 $urls = isset($controls['urls']) && is_array($controls['urls']) ? $controls['urls'] : array();
305 $labels = isset($controls['labels']) && is_array($controls['labels']) ? $controls['labels'] : array();
306
307 $controlsHtml = $this->tpl('viewStatsDiagnosticsLatencyControls.html');
308 $controlsHtml = $this->f->str_replace('{value_text}', esc_html($this->valueToString($controls['value_text'] ?? '')), $controlsHtml);
309 $controlsHtml = $this->f->str_replace('{url_250}', esc_url($this->valueToString($urls[250] ?? '')), $controlsHtml);
310 $controlsHtml = $this->f->str_replace('{url_500}', esc_url($this->valueToString($urls[500] ?? '')), $controlsHtml);
311 $controlsHtml = $this->f->str_replace('{url_900}', esc_url($this->valueToString($urls[900] ?? '')), $controlsHtml);
312 $controlsHtml = $this->f->str_replace('{url_0}', esc_url($this->valueToString($urls[0] ?? '')), $controlsHtml);
313 $controlsHtml = $this->f->str_replace('{label_250}', esc_html($this->valueToString($labels[250] ?? '')), $controlsHtml);
314 $controlsHtml = $this->f->str_replace('{label_500}', esc_html($this->valueToString($labels[500] ?? '')), $controlsHtml);
315 $controlsHtml = $this->f->str_replace('{label_900}', esc_html($this->valueToString($labels[900] ?? '')), $controlsHtml);
316 $controlsHtml = $this->f->str_replace('{label_disable}', esc_html($this->valueToString($labels[0] ?? '')), $controlsHtml);
317
318 return $controlsHtml;
319 }
320
321 /**
322 * @param array<mixed> $row
323 * @param string $key
324 * @return string
325 */
326 private function readString(array $row, string $key): string {
327 return $this->valueToString($row[$key] ?? '');
328 }
329
330 /**
331 * @param mixed $value
332 * @return string
333 */
334 private function valueToString($value): string {
335 if (is_string($value)) {
336 return $value;
337 }
338 if (is_int($value) || is_float($value) || is_bool($value)) {
339 return (string)$value;
340 }
341
342 return '';
343 }
344 }
345