| 1 |
<?php |
| 2 |
|
| 3 |
if (!defined('ABSPATH')) { |
| 4 |
exit; |
| 5 |
} |
| 6 |
|
| 7 |
/** |
| 8 |
* ViewTrait_Stats methods. |
| 9 |
*/ |
| 10 |
trait ViewTrait_Stats { |
| 11 |
|
| 12 |
|
| 13 |
/** |
| 14 |
* Output the stats page. |
| 15 |
* @return void |
| 16 |
*/ |
| 17 |
function outputAdminStatsPage() { |
| 18 |
global $abj404view; |
| 19 |
|
| 20 |
$statsSnapshot = $this->statsRepository->getStatsDashboardSnapshot(true); |
| 21 |
$statsData = $statsSnapshot['data']; |
| 22 |
$statsHash = $statsSnapshot['hash']; |
| 23 |
|
| 24 |
// Main container |
| 25 |
echo "<div class=\"abj404-container\">"; |
| 26 |
echo "<div class=\"abj404-settings-content\">"; |
| 27 |
|
| 28 |
// Header row with Expand All button |
| 29 |
echo "<div class=\"abj404-header-row\">"; |
| 30 |
echo "<h2>" . esc_html__('Statistics', '404-solution') . "</h2>"; |
| 31 |
echo "<div class=\"abj404-header-controls\">"; |
| 32 |
echo '<button type="button" id="abj404-expand-collapse-all" class="button">'; |
| 33 |
echo esc_html__('Expand All', '404-solution'); |
| 34 |
echo '</button>'; |
| 35 |
echo "</div>"; |
| 36 |
echo "</div>"; |
| 37 |
|
| 38 |
// Config for stale-while-refresh stats snapshot updates (no visible table overwrite). |
| 39 |
echo '<div class="abj404-stats-refresh-config" style="display:none"' |
| 40 |
. ' data-stats-refresh-enabled="1"' |
| 41 |
. ' data-stats-refresh-action="ajaxRefreshStatsDashboard"' |
| 42 |
. ' data-stats-refresh-nonce="' . esc_attr(wp_create_nonce('abj404_refreshStatsDashboard')) . '"' |
| 43 |
. ' data-stats-refresh-current-hash="' . esc_attr($statsHash) . '"' |
| 44 |
. ' data-stats-refresh-available-text="' . esc_attr(__('Refresh available', '404-solution')) . '"></div>'; |
| 45 |
|
| 46 |
// Flow layout for stats cards |
| 47 |
echo "<div class=\"abj404-flow-layout\">"; |
| 48 |
|
| 49 |
// Redirects Statistics Card |
| 50 |
$redirectStats = (is_array($statsData) && isset($statsData['redirects']) && is_array($statsData['redirects'])) |
| 51 |
? $statsData['redirects'] |
| 52 |
: array(); |
| 53 |
$auto301 = intval($redirectStats['auto301'] ?? 0); |
| 54 |
$auto302 = intval($redirectStats['auto302'] ?? 0); |
| 55 |
$manual301 = intval($redirectStats['manual301'] ?? 0); |
| 56 |
$manual302 = intval($redirectStats['manual302'] ?? 0); |
| 57 |
$trashed = intval($redirectStats['trashed'] ?? 0); |
| 58 |
|
| 59 |
$total = $auto301 + $auto302 + $manual301 + $manual302 + $trashed; |
| 60 |
|
| 61 |
$content = ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/html/statsRedirectsBox.html"); |
| 62 |
|
| 63 |
// In Simple mode, replace technical "301"/"302" labels with plain language |
| 64 |
if ($this->logic->getSettingsMode() === 'simple') { |
| 65 |
$content = $this->f->str_replace('{Automatic 301 Redirects}', esc_html__('Automatic Permanent Redirects', '404-solution'), $content); |
| 66 |
$content = $this->f->str_replace('{Automatic 302 Redirects}', esc_html__('Automatic Temporary Redirects', '404-solution'), $content); |
| 67 |
$content = $this->f->str_replace('{Manual 301 Redirects}', esc_html__('Manual Permanent Redirects', '404-solution'), $content); |
| 68 |
$content = $this->f->str_replace('{Manual 302 Redirects}', esc_html__('Manual Temporary Redirects', '404-solution'), $content); |
| 69 |
} |
| 70 |
|
| 71 |
$content = $this->f->str_replace('{auto301}', esc_html((string)$auto301), $content); |
| 72 |
$content = $this->f->str_replace('{auto302}', esc_html((string)$auto302), $content); |
| 73 |
$content = $this->f->str_replace('{manual301}', esc_html((string)$manual301), $content); |
| 74 |
$content = $this->f->str_replace('{manual302}', esc_html((string)$manual302), $content); |
| 75 |
$content = $this->f->str_replace('{trashed}', esc_html((string)$trashed), $content); |
| 76 |
$content = $this->f->str_replace('{total}', esc_html((string)$total), $content); |
| 77 |
$content = $this->f->doNormalReplacements($content); |
| 78 |
$abj404view->echoOptionsSection('stats-redirects', 'abj404-redirectStats', __('Redirects', '404-solution'), $content, true, $abj404view->getCardIcon('chart')); |
| 79 |
|
| 80 |
// Captured URLs Statistics Card |
| 81 |
$capturedStats = (is_array($statsData) && isset($statsData['captured']) && is_array($statsData['captured'])) |
| 82 |
? $statsData['captured'] |
| 83 |
: array(); |
| 84 |
$captured = intval($capturedStats['captured'] ?? 0); |
| 85 |
$ignored = intval($capturedStats['ignored'] ?? 0); |
| 86 |
$trashed = intval($capturedStats['trashed'] ?? 0); |
| 87 |
|
| 88 |
$total = $captured + $ignored + $trashed; |
| 89 |
|
| 90 |
$content = ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/html/statsCapturedURLsBox.html"); |
| 91 |
$content = $this->f->str_replace('{captured}', esc_html((string)$captured), $content); |
| 92 |
$content = $this->f->str_replace('{ignored}', esc_html((string)$ignored), $content); |
| 93 |
$content = $this->f->str_replace('{trashed}', esc_html((string)$trashed), $content); |
| 94 |
$content = $this->f->str_replace('{total}', esc_html((string)$total), $content); |
| 95 |
$content = $this->f->doNormalReplacements($content); |
| 96 |
$abj404view->echoOptionsSection('stats-captured', 'abj404-capturedStats', __('Captured URLs', '404-solution'), $content, true, $abj404view->getCardIcon('warning')); |
| 97 |
|
| 98 |
// Periodic Stats Cards |
| 99 |
$periodicStats = (is_array($statsData) && isset($statsData['periods']) && is_array($statsData['periods'])) |
| 100 |
? $statsData['periods'] |
| 101 |
: array(); |
| 102 |
$periodMeta = array( |
| 103 |
array('title' => __("Today's Stats", '404-solution'), 'key' => 'today'), |
| 104 |
array('title' => __("This Month", '404-solution'), 'key' => 'month'), |
| 105 |
array('title' => __("This Year", '404-solution'), 'key' => 'year'), |
| 106 |
array('title' => __("All Stats", '404-solution'), 'key' => 'all'), |
| 107 |
); |
| 108 |
|
| 109 |
for ($x = 0; $x <= 3; $x++) { |
| 110 |
$title = $periodMeta[$x]['title']; |
| 111 |
$periodKey = $periodMeta[$x]['key']; |
| 112 |
$periodStats = (is_array($periodicStats) && isset($periodicStats[$periodKey]) && is_array($periodicStats[$periodKey])) |
| 113 |
? $periodicStats[$periodKey] |
| 114 |
: array(); |
| 115 |
$disp404 = intval($periodStats['disp404'] ?? 0); |
| 116 |
$distinct404 = intval($periodStats['distinct404'] ?? 0); |
| 117 |
$visitors404 = intval($periodStats['visitors404'] ?? 0); |
| 118 |
$refer404 = intval($periodStats['refer404'] ?? 0); |
| 119 |
$redirected = intval($periodStats['redirected'] ?? 0); |
| 120 |
$distinctredirected = intval($periodStats['distinctredirected'] ?? 0); |
| 121 |
$distinctvisitors = intval($periodStats['distinctvisitors'] ?? 0); |
| 122 |
$distinctrefer = intval($periodStats['distinctrefer'] ?? 0); |
| 123 |
|
| 124 |
$content = ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/html/statsPeriodicBox.html"); |
| 125 |
$content = $this->f->str_replace('{disp404}', esc_html((string)$disp404), $content); |
| 126 |
$content = $this->f->str_replace('{distinct404}', esc_html((string)$distinct404), $content); |
| 127 |
$content = $this->f->str_replace('{visitors404}', esc_html((string)$visitors404), $content); |
| 128 |
$content = $this->f->str_replace('{refer404}', esc_html((string)$refer404), $content); |
| 129 |
$content = $this->f->str_replace('{redirected}', esc_html((string)$redirected), $content); |
| 130 |
$content = $this->f->str_replace('{distinctredirected}', esc_html((string)$distinctredirected), $content); |
| 131 |
$content = $this->f->str_replace('{distinctvisitors}', esc_html((string)$distinctvisitors), $content); |
| 132 |
$content = $this->f->str_replace('{distinctrefer}', esc_html((string)$distinctrefer), $content); |
| 133 |
$content = $this->f->doNormalReplacements($content); |
| 134 |
$abj404view->echoOptionsSection('stats-periodic-' . $x, 'abj404-stats' . $x, $title, $content, ($x == 0), $abj404view->getCardIcon('clock')); |
| 135 |
} |
| 136 |
|
| 137 |
echo "</div>"; // Close flow layout |
| 138 |
|
| 139 |
// Match Confidence distribution card (full-width) |
| 140 |
$this->echoConfidenceDistributionSection(); |
| 141 |
|
| 142 |
// Trend Analytics section (full-width, below the flow layout cards) |
| 143 |
$this->echoTrendsSection(); |
| 144 |
|
| 145 |
// Broken Internal Links section |
| 146 |
$this->echoBrokenInternalLinksSection(); |
| 147 |
|
| 148 |
echo "</div>"; // Close settings content |
| 149 |
echo "</div>"; // Close container |
| 150 |
} |
| 151 |
|
| 152 |
/** |
| 153 |
* Output the Match Confidence distribution card on the Stats page. |
| 154 |
* Queries the redirects table for score band counts and renders a Chart.js doughnut. |
| 155 |
* @return void |
| 156 |
*/ |
| 157 |
private function echoConfidenceDistributionSection() { |
| 158 |
global $abj404view, $wpdb; |
| 159 |
|
| 160 |
if (!isset($wpdb)) { |
| 161 |
return; |
| 162 |
} |
| 163 |
|
| 164 |
$dbCore = abj_service('db_core'); |
| 165 |
$redirectsTable = $dbCore->doTableNameReplacements('{wp_abj404_redirects}'); |
| 166 |
|
| 167 |
// Query score distribution bands. Route through the DAO so the |
| 168 |
// 5x SUM(CASE...) aggregate inherits the centralized 60s SELECT |
| 169 |
// timeout — the redirects table can be very large on busy sites. |
| 170 |
$sql = "SELECT |
| 171 |
SUM(CASE WHEN score IS NULL THEN 1 ELSE 0 END) AS manual_count, |
| 172 |
SUM(CASE WHEN score >= 80 THEN 1 ELSE 0 END) AS high_count, |
| 173 |
SUM(CASE WHEN score >= 50 AND score < 80 THEN 1 ELSE 0 END) AS medium_count, |
| 174 |
SUM(CASE WHEN score IS NOT NULL AND score < 50 THEN 1 ELSE 0 END) AS low_count, |
| 175 |
AVG(score) AS avg_score |
| 176 |
FROM `{$redirectsTable}` |
| 177 |
WHERE disabled = %d AND status != %d"; |
| 178 |
|
| 179 |
$result = $dbCore->queryAndGetResults($sql, array('query_params' => array(0, 0))); |
| 180 |
if (!empty($result['timed_out']) || (isset($result['last_error']) && $result['last_error'] != '')) { |
| 181 |
return; |
| 182 |
} |
| 183 |
$rows = is_array($result['rows'] ?? null) ? $result['rows'] : array(); |
| 184 |
if (empty($rows) || !is_array($rows[0] ?? null)) { |
| 185 |
return; |
| 186 |
} |
| 187 |
$row = $rows[0]; |
| 188 |
|
| 189 |
$highCount = (int)($row['high_count'] ?? 0); |
| 190 |
$mediumCount = (int)($row['medium_count'] ?? 0); |
| 191 |
$lowCount = (int)($row['low_count'] ?? 0); |
| 192 |
$manualCount = (int)($row['manual_count'] ?? 0); |
| 193 |
$avgScore = ($row['avg_score'] !== null) ? round((float)$row['avg_score'], 1) : null; |
| 194 |
|
| 195 |
$total = $highCount + $mediumCount + $lowCount + $manualCount; |
| 196 |
if ($total === 0) { |
| 197 |
return; |
| 198 |
} |
| 199 |
|
| 200 |
$labelHigh = esc_html__('High (≥80%)', '404-solution'); |
| 201 |
$labelMedium = esc_html__('Medium (50–79%)', '404-solution'); |
| 202 |
$labelLow = esc_html__('Low (<50%)', '404-solution'); |
| 203 |
$labelManual = esc_html__('Manual (no score)', '404-solution'); |
| 204 |
|
| 205 |
$avgLabel = ($avgScore !== null) |
| 206 |
? sprintf( |
| 207 |
'<strong>' . esc_html__('Avg confidence: %s%%', '404-solution') . '</strong>', |
| 208 |
esc_html(number_format($avgScore, 1)) |
| 209 |
) |
| 210 |
: ''; |
| 211 |
|
| 212 |
$content = '<div class="abj404-confidence-dist">'; |
| 213 |
$content .= '<p class="abj404-confidence-avg">' . $avgLabel . '</p>'; |
| 214 |
$content .= '<canvas id="abj404-chart-confidence" style="max-height:200px;max-width:400px;"></canvas>'; |
| 215 |
$content .= '<ul class="abj404-confidence-legend">'; |
| 216 |
$content .= '<li><span class="abj404-legend-dot abj404-conf-high"></span>' . esc_html($labelHigh) . ' <strong>' . esc_html((string)$highCount) . '</strong></li>'; |
| 217 |
$content .= '<li><span class="abj404-legend-dot abj404-conf-medium"></span>' . esc_html($labelMedium) . ' <strong>' . esc_html((string)$mediumCount) . '</strong></li>'; |
| 218 |
$content .= '<li><span class="abj404-legend-dot abj404-conf-low"></span>' . esc_html($labelLow) . ' <strong>' . esc_html((string)$lowCount) . '</strong></li>'; |
| 219 |
$content .= '<li><span class="abj404-legend-dot abj404-conf-manual"></span>' . esc_html($labelManual) . ' <strong>' . esc_html((string)$manualCount) . '</strong></li>'; |
| 220 |
$content .= '</ul>'; |
| 221 |
$content .= '</div>'; |
| 222 |
|
| 223 |
$content .= '<style>' |
| 224 |
. '.abj404-confidence-dist { display: flex; align-items: center; gap: 32px; flex-wrap: wrap; }' |
| 225 |
. '.abj404-confidence-avg { font-size: 14px; margin-bottom: 8px; }' |
| 226 |
. '.abj404-confidence-legend { list-style: none; margin: 0; padding: 0; }' |
| 227 |
. '.abj404-confidence-legend li { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; font-size: 13px; }' |
| 228 |
. '.abj404-legend-dot { display: inline-block; width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; }' |
| 229 |
. '.abj404-conf-high { background: #28a745; }' |
| 230 |
. '.abj404-conf-medium { background: #ffc107; }' |
| 231 |
. '.abj404-conf-low { background: #dc3545; }' |
| 232 |
. '.abj404-conf-manual { background: #adb5bd; }' |
| 233 |
. '</style>'; |
| 234 |
|
| 235 |
$content .= '<script>' |
| 236 |
. '(function() {' |
| 237 |
. ' function renderConfidenceChart() {' |
| 238 |
. ' var ctx = document.getElementById("abj404-chart-confidence");' |
| 239 |
. ' if (!ctx || !window.Chart) return;' |
| 240 |
. ' new Chart(ctx, {' |
| 241 |
. ' type: "doughnut",' |
| 242 |
. ' data: {' |
| 243 |
. ' labels: [' . json_encode($labelHigh) . ',' . json_encode($labelMedium) . ',' . json_encode($labelLow) . ',' . json_encode($labelManual) . '],' |
| 244 |
. ' datasets: [{ data: [' . $highCount . ',' . $mediumCount . ',' . $lowCount . ',' . $manualCount . '],' |
| 245 |
. ' backgroundColor: ["#28a745","#ffc107","#dc3545","#adb5bd"] }]' |
| 246 |
. ' },' |
| 247 |
. ' options: { responsive: true, plugins: { legend: { display: false } } }' |
| 248 |
. ' });' |
| 249 |
. ' }' |
| 250 |
. ' if (window.Chart) { renderConfidenceChart(); }' |
| 251 |
. ' else { document.addEventListener("abj404ChartJsLoaded", renderConfidenceChart); }' |
| 252 |
. '})();' |
| 253 |
. '</script>'; |
| 254 |
|
| 255 |
$abj404view->echoOptionsSection( |
| 256 |
'stats-confidence', |
| 257 |
'abj404-confidenceSection', |
| 258 |
__('Match Confidence', '404-solution'), |
| 259 |
$content, |
| 260 |
false, |
| 261 |
$abj404view->getCardIcon('check') |
| 262 |
); |
| 263 |
} |
| 264 |
|
| 265 |
/** |
| 266 |
* Output the Trends (time-series charts) section on the Stats page. |
| 267 |
* @return void |
| 268 |
*/ |
| 269 |
private function echoTrendsSection() { |
| 270 |
global $abj404view; |
| 271 |
|
| 272 |
$trendNonce = wp_create_nonce('abj404_trendData'); |
| 273 |
$ajaxUrl = admin_url('admin-ajax.php'); |
| 274 |
|
| 275 |
$label7d = esc_html__('7 days', '404-solution'); |
| 276 |
$label30d = esc_html__('30 days', '404-solution'); |
| 277 |
$label90d = esc_html__('90 days', '404-solution'); |
| 278 |
|
| 279 |
$trendsContent = '<div id="abj404-trends-container">'; |
| 280 |
$trendsContent .= '<div class="abj404-trends-period-selector" role="group" aria-label="' . esc_attr__('Period', '404-solution') . '">'; |
| 281 |
$trendsContent .= '<label class="abj404-trends-period-label"><input type="radio" name="abj404_trend_period" value="7"> ' . $label7d . '</label>'; |
| 282 |
$trendsContent .= '<label class="abj404-trends-period-label"><input type="radio" name="abj404_trend_period" value="30" checked> ' . $label30d . '</label>'; |
| 283 |
$trendsContent .= '<label class="abj404-trends-period-label"><input type="radio" name="abj404_trend_period" value="90"> ' . $label90d . '</label>'; |
| 284 |
$trendsContent .= '</div>'; |
| 285 |
$trendsContent .= '<p class="abj404-trends-loading">' . esc_html__('Loading chart data…', '404-solution') . '</p>'; |
| 286 |
$trendsContent .= '<div id="abj404-trends-charts" style="display:none">'; |
| 287 |
$trendsContent .= '<div class="abj404-trend-chart-wrap"><canvas id="abj404-chart-404s"></canvas></div>'; |
| 288 |
$trendsContent .= '<div class="abj404-trend-chart-wrap"><canvas id="abj404-chart-redirects"></canvas></div>'; |
| 289 |
$trendsContent .= '<div class="abj404-trend-chart-wrap"><canvas id="abj404-chart-captures"></canvas></div>'; |
| 290 |
$trendsContent .= '</div>'; |
| 291 |
$trendsContent .= '<p id="abj404-trends-error" style="display:none;color:#d63638">' |
| 292 |
. esc_html__('Could not load chart data.', '404-solution') . '</p>'; |
| 293 |
$trendsContent .= '</div>'; |
| 294 |
|
| 295 |
$trendsContent .= '<style>' |
| 296 |
. '.abj404-trends-period-selector { margin-bottom: 16px; display: flex; gap: 16px; flex-wrap: wrap; align-items: center; }' |
| 297 |
. '.abj404-trends-period-label { cursor: pointer; font-weight: 500; }' |
| 298 |
. '.abj404-trends-period-label input { margin-right: 4px; }' |
| 299 |
. '.abj404-trend-chart-wrap { margin-bottom: 24px; }' |
| 300 |
. '.abj404-trends-loading { color: #646970; font-style: italic; }' |
| 301 |
. '</style>'; |
| 302 |
|
| 303 |
// Inline JS: load Chart.js from CDN then fetch data and render charts. |
| 304 |
$label404 = esc_js(__('404 Hits per Day', '404-solution')); |
| 305 |
$labelRedirect = esc_js(__('Redirects per Day', '404-solution')); |
| 306 |
$labelCapture = esc_js(__('New Captures per Day', '404-solution')); |
| 307 |
$ajaxUrlEsc = esc_js($ajaxUrl); |
| 308 |
$nonceEsc = esc_js($trendNonce); |
| 309 |
|
| 310 |
$trendsContent .= '<script>' |
| 311 |
. '(function() {' |
| 312 |
. ' var ajaxUrl = "' . $ajaxUrlEsc . '";' |
| 313 |
. ' var nonce = "' . $nonceEsc . '";' |
| 314 |
. ' function loadChartJs(cb) {' |
| 315 |
. ' if (window.Chart) { cb(); return; }' |
| 316 |
. ' var s = document.createElement("script");' |
| 317 |
. ' s.src = "https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js";' |
| 318 |
. ' s.onload = function() {' |
| 319 |
. ' document.dispatchEvent(new Event("abj404ChartJsLoaded"));' |
| 320 |
. ' cb();' |
| 321 |
. ' };' |
| 322 |
. ' s.onerror = function() {' |
| 323 |
. ' var loadEl = document.querySelector(".abj404-trends-loading");' |
| 324 |
. ' if (loadEl) loadEl.style.display = "none";' |
| 325 |
. ' var errEl = document.getElementById("abj404-trends-error");' |
| 326 |
. ' if (errEl) errEl.style.display = "";' |
| 327 |
. ' };' |
| 328 |
. ' document.head.appendChild(s);' |
| 329 |
. ' }' |
| 330 |
. ' function buildChart(canvasId, label, color, labels, values) {' |
| 331 |
. ' var ctx = document.getElementById(canvasId);' |
| 332 |
. ' if (!ctx) return null;' |
| 333 |
. ' return new Chart(ctx, {' |
| 334 |
. ' type: "line",' |
| 335 |
. ' data: {' |
| 336 |
. ' labels: labels,' |
| 337 |
. ' datasets: [{' |
| 338 |
. ' label: label,' |
| 339 |
. ' data: values,' |
| 340 |
. ' borderColor: color,' |
| 341 |
. ' backgroundColor: color.replace("rgb(", "rgba(").replace(")", ", 0.15)"),' |
| 342 |
. ' tension: 0.3,' |
| 343 |
. ' fill: true,' |
| 344 |
. ' pointRadius: 3' |
| 345 |
. ' }]' |
| 346 |
. ' },' |
| 347 |
. ' options: {' |
| 348 |
. ' responsive: true,' |
| 349 |
. ' plugins: { legend: { display: true } },' |
| 350 |
. ' scales: { y: { beginAtZero: true, ticks: { precision: 0 } } }' |
| 351 |
. ' }' |
| 352 |
. ' });' |
| 353 |
. ' }' |
| 354 |
. ' var chartInstances = {};' |
| 355 |
. ' function getSelectedDays() {' |
| 356 |
. ' var radios = document.querySelectorAll("input[name=abj404_trend_period]");' |
| 357 |
. ' for (var i = 0; i < radios.length; i++) {' |
| 358 |
. ' if (radios[i].checked) return parseInt(radios[i].value, 10);' |
| 359 |
. ' }' |
| 360 |
. ' return 30;' |
| 361 |
. ' }' |
| 362 |
. ' function destroyCharts() {' |
| 363 |
. ' ["abj404-chart-404s","abj404-chart-redirects","abj404-chart-captures"].forEach(function(id) {' |
| 364 |
. ' if (chartInstances[id]) { chartInstances[id].destroy(); delete chartInstances[id]; }' |
| 365 |
. ' });' |
| 366 |
. ' }' |
| 367 |
. ' function fetchTrendData(days, allowRetry) {' |
| 368 |
. ' return fetch(ajaxUrl + "?action=abj404getTrendData&nonce=" + encodeURIComponent(nonce) + "&days=" + days)' |
| 369 |
. ' .then(function(r) {' |
| 370 |
// B20: a 12-24h-idle nonce expires; admin-ajax replies 403. |
| 371 |
// Mint a fresh nonce via the shared refresh helper (if loaded) |
| 372 |
// and retry once. allowRetry guards against an infinite loop. |
| 373 |
. ' if (r.status === 403 && allowRetry !== false && window.abj404NonceRefresh) {' |
| 374 |
. ' return window.abj404NonceRefresh.fetchFresh().then(function(freshNonces) {' |
| 375 |
. ' if (freshNonces && freshNonces["abj404_trendData"]) {' |
| 376 |
. ' nonce = freshNonces["abj404_trendData"];' |
| 377 |
. ' }' |
| 378 |
. ' return fetchTrendData(days, false);' |
| 379 |
. ' });' |
| 380 |
. ' }' |
| 381 |
. ' return r.json();' |
| 382 |
. ' });' |
| 383 |
. ' }' |
| 384 |
. ' function fetchAndRender() {' |
| 385 |
. ' var days = getSelectedDays();' |
| 386 |
. ' var loadEl = document.querySelector(".abj404-trends-loading");' |
| 387 |
. ' var errEl = document.getElementById("abj404-trends-error");' |
| 388 |
. ' var chartsEl = document.getElementById("abj404-trends-charts");' |
| 389 |
. ' if (loadEl) loadEl.style.display = "";' |
| 390 |
. ' if (errEl) errEl.style.display = "none";' |
| 391 |
. ' if (chartsEl) chartsEl.style.display = "none";' |
| 392 |
. ' destroyCharts();' |
| 393 |
. ' fetchTrendData(days, true)' |
| 394 |
. ' .then(function(resp) {' |
| 395 |
. ' if (loadEl) loadEl.style.display = "none";' |
| 396 |
. ' if (!resp || !resp.success || !Array.isArray(resp.data)) {' |
| 397 |
. ' if (errEl) errEl.style.display = "";' |
| 398 |
. ' return;' |
| 399 |
. ' }' |
| 400 |
. ' var rows = resp.data;' |
| 401 |
. ' var labels = rows.map(function(r) { return r.date; });' |
| 402 |
. ' var vals404 = rows.map(function(r) { return r.hits_404; });' |
| 403 |
. ' var valsRedir = rows.map(function(r) { return r.hits_redirect; });' |
| 404 |
. ' var valsCapt = rows.map(function(r) { return r.new_captures; });' |
| 405 |
. ' if (chartsEl) chartsEl.style.display = "";' |
| 406 |
. ' chartInstances["abj404-chart-404s"] = buildChart("abj404-chart-404s", "' . $label404 . '", "rgb(0,115,170)", labels, vals404);' |
| 407 |
. ' chartInstances["abj404-chart-redirects"] = buildChart("abj404-chart-redirects", "' . $labelRedirect . '", "rgb(70,170,100)", labels, valsRedir);' |
| 408 |
. ' chartInstances["abj404-chart-captures"] = buildChart("abj404-chart-captures", "' . $labelCapture . '", "rgb(220,100,50)", labels, valsCapt);' |
| 409 |
. ' })' |
| 410 |
. ' .catch(function() {' |
| 411 |
. ' if (loadEl) loadEl.style.display = "none";' |
| 412 |
. ' if (errEl) errEl.style.display = "";' |
| 413 |
. ' });' |
| 414 |
. ' }' |
| 415 |
. ' function onPeriodChange() { fetchAndRender(); }' |
| 416 |
. ' document.addEventListener("DOMContentLoaded", function() {' |
| 417 |
. ' loadChartJs(fetchAndRender);' |
| 418 |
. ' document.querySelectorAll("input[name=abj404_trend_period]").forEach(function(r) {' |
| 419 |
. ' r.addEventListener("change", onPeriodChange);' |
| 420 |
. ' });' |
| 421 |
. ' });' |
| 422 |
. '})();' |
| 423 |
. '</script>'; |
| 424 |
|
| 425 |
$abj404view->echoOptionsSection( |
| 426 |
'stats-trends', |
| 427 |
'abj404-trendsSection', |
| 428 |
__('Trend Analytics', '404-solution'), |
| 429 |
$trendsContent, |
| 430 |
false, |
| 431 |
$abj404view->getCardIcon('chart') |
| 432 |
); |
| 433 |
} |
| 434 |
|
| 435 |
/** |
| 436 |
* Output the Broken Internal Links section on the Stats page (if results are cached). |
| 437 |
* @return void |
| 438 |
*/ |
| 439 |
private function echoBrokenInternalLinksSection() { |
| 440 |
global $abj404view; |
| 441 |
|
| 442 |
if (!class_exists('ABJ_404_Solution_InternalLinkScanner')) { |
| 443 |
return; |
| 444 |
} |
| 445 |
|
| 446 |
$scanner = new ABJ_404_Solution_InternalLinkScanner(); |
| 447 |
$results = $scanner->getCachedResults(); |
| 448 |
|
| 449 |
if ($results === false || !is_array($results)) { |
| 450 |
// No cached results yet — nothing to show. |
| 451 |
return; |
| 452 |
} |
| 453 |
|
| 454 |
if (empty($results)) { |
| 455 |
$content = '<p>' . esc_html__('No broken internal links found.', '404-solution') . '</p>'; |
| 456 |
} else { |
| 457 |
$postCount = count(array_unique(array_column($results, 'post_id'))); |
| 458 |
$content = '<p>' . esc_html(sprintf( |
| 459 |
/* translators: 1: number of broken links, 2: number of posts/pages */ |
| 460 |
__('Found %1$d broken internal link(s) across %2$d post(s)/page(s).', '404-solution'), |
| 461 |
count($results), |
| 462 |
$postCount |
| 463 |
)) . '</p>'; |
| 464 |
$content .= '<table class="widefat striped"><thead><tr>' |
| 465 |
. '<th>' . esc_html__('Post/Page', '404-solution') . '</th>' |
| 466 |
. '<th>' . esc_html__('Broken URL', '404-solution') . '</th>' |
| 467 |
. '<th>' . esc_html__('404 Hits', '404-solution') . '</th>' |
| 468 |
. '</tr></thead><tbody>'; |
| 469 |
foreach ($results as $item) { |
| 470 |
if (!is_array($item)) { |
| 471 |
continue; |
| 472 |
} |
| 473 |
$postTitle = (string)$item['post_title']; |
| 474 |
$brokenUrl = (string)$item['broken_url']; |
| 475 |
$hitCount = intval($item['hit_count']); |
| 476 |
$postId = intval($item['post_id']); |
| 477 |
$editLink = ($postId > 0) ? get_edit_post_link($postId) : ''; |
| 478 |
$content .= '<tr>'; |
| 479 |
if ($editLink) { |
| 480 |
$content .= '<td><a href="' . esc_url($editLink) . '">' . esc_html($postTitle) . '</a></td>'; |
| 481 |
} else { |
| 482 |
$content .= '<td>' . esc_html($postTitle) . '</td>'; |
| 483 |
} |
| 484 |
$content .= '<td><code>' . esc_html($brokenUrl) . '</code></td>'; |
| 485 |
$content .= '<td>' . esc_html((string)$hitCount) . '</td>'; |
| 486 |
$content .= '</tr>'; |
| 487 |
} |
| 488 |
$content .= '</tbody></table>'; |
| 489 |
} |
| 490 |
|
| 491 |
$abj404view->echoOptionsSection( |
| 492 |
'stats-broken-links', |
| 493 |
'abj404-brokenLinksSection', |
| 494 |
__('Broken Internal Links', '404-solution'), |
| 495 |
$content, |
| 496 |
false, |
| 497 |
$abj404view->getCardIcon('warning') |
| 498 |
); |
| 499 |
} |
| 500 |
|
| 501 |
/** @return void */ |
| 502 |
function echoAdminDebugFile() { |
| 503 |
if ($this->logic->userIsPluginAdmin()) { |
| 504 |
$filesToEcho = array($this->logger->getDebugFilePath(), |
| 505 |
$this->logger->getDebugFilePathOld()); |
| 506 |
for ($i = 0; $i < count($filesToEcho); $i++) { |
| 507 |
$currentFile = $filesToEcho[$i]; |
| 508 |
echo "<div style=\"clear: both;\">"; |
| 509 |
echo "<BR/>Contents of: " . $currentFile . ": <BR/><BR/>"; |
| 510 |
// read the file and replace new lines with <BR/>. |
| 511 |
$this->echoFileContents($currentFile); |
| 512 |
echo "</div>"; |
| 513 |
} |
| 514 |
|
| 515 |
} else { |
| 516 |
echo "Non-admin request to view debug file."; |
| 517 |
$current_user = ABJ_404_Solution_UserRef::fromWpUser(wp_get_current_user()); |
| 518 |
$userLogin = $current_user !== null ? $current_user->getLogin() : ''; |
| 519 |
$userDisplay = $current_user !== null ? $current_user->getDisplayName() : ''; |
| 520 |
$userEmail = $current_user !== null ? $current_user->getEmail() : ''; |
| 521 |
$userId = $current_user !== null ? $current_user->getId() : 0; |
| 522 |
$userInfo = "Login: " . $userLogin . ", display name: " . |
| 523 |
$userDisplay . ", Email: " . $userEmail . |
| 524 |
", UserID: " . $userId; |
| 525 |
$this->logger->infoMessage("Non-admin request to view debug file. User info: " . |
| 526 |
$userInfo); |
| 527 |
} |
| 528 |
} |
| 529 |
|
| 530 |
/** |
| 531 |
* @param string $fileName |
| 532 |
* @return void |
| 533 |
*/ |
| 534 |
function echoFileContents($fileName) { |
| 535 |
|
| 536 |
if (!is_string($fileName)) { |
| 537 |
$fileName = ''; |
| 538 |
} |
| 539 |
|
| 540 |
if (file_exists($fileName)) { |
| 541 |
$linesRead = 0; |
| 542 |
$handle = null; |
| 543 |
try { |
| 544 |
if ($handle = fopen($fileName, "r")) { |
| 545 |
// read the file one line at a time. |
| 546 |
while (($line = fgets($handle)) !== false) { |
| 547 |
$linesRead++; |
| 548 |
echo nl2br(esc_html($line)); |
| 549 |
|
| 550 |
if ($linesRead > 1000000) { |
| 551 |
echo "<BR/><BR/>Read " . $linesRead . " lines. Download debug file to see more."; |
| 552 |
break; |
| 553 |
} |
| 554 |
} |
| 555 |
} else { |
| 556 |
$this->logger->errorMessage("Error opening debug file."); |
| 557 |
} |
| 558 |
|
| 559 |
} catch (Exception $e) { |
| 560 |
$this->logger->errorMessage("Error while reading debug file.", $e); |
| 561 |
} |
| 562 |
|
| 563 |
if ($handle != null) { |
| 564 |
fclose($handle); |
| 565 |
} |
| 566 |
|
| 567 |
} else { |
| 568 |
echo nl2br(__('(The log file does not exist.)', '404-solution')); |
| 569 |
} |
| 570 |
} |
| 571 |
|
| 572 |
/** |
| 573 |
* Display the tools page. |
| 574 |
* @return void |
| 575 |
*/ |
| 576 |
function echoAdminToolsPage() { |
| 577 |
global $abj404view; |
| 578 |
|
| 579 |
// Main container |
| 580 |
echo "<div class=\"abj404-container\">"; |
| 581 |
echo "<div class=\"abj404-settings-content\">"; |
| 582 |
|
| 583 |
// Header row with Expand All button |
| 584 |
echo "<div class=\"abj404-header-row\">"; |
| 585 |
echo "<h2>" . esc_html__('Tools', '404-solution') . "</h2>"; |
| 586 |
echo "<div class=\"abj404-header-controls\">"; |
| 587 |
echo '<button type="button" id="abj404-expand-collapse-all" class="button">'; |
| 588 |
echo esc_html__('Expand All', '404-solution'); |
| 589 |
echo '</button>'; |
| 590 |
echo "</div>"; |
| 591 |
echo "</div>"; |
| 592 |
|
| 593 |
// Export Card |
| 594 |
$link = wp_nonce_url("?page=" . ABJ404_PP . "&subpage=abj404_tools", "abj404_exportRedirects"); |
| 595 |
$html = ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/html/toolsExportForm.html"); |
| 596 |
$html = $this->f->str_replace('{toolsExportRedirectsLink}', $link, $html); |
| 597 |
$html = $this->f->doNormalReplacements($html); |
| 598 |
$abj404view->echoOptionsSection('tools-export', 'abj404-exportRedirects', __('Export', '404-solution'), $html, true, $abj404view->getCardIcon('download')); |
| 599 |
|
| 600 |
// Import Card |
| 601 |
$link = wp_nonce_url("?page=" . ABJ404_PP . "&subpage=abj404_tools", "abj404_importRedirectsFile"); |
| 602 |
$html = ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/html/toolsImportForm.html"); |
| 603 |
$html = $this->f->str_replace('{toolsImportRedirectsLink}', $link, $html); |
| 604 |
$html = $this->f->doNormalReplacements($html); |
| 605 |
$abj404view->echoOptionsSection('tools-import', 'abj404-importRedirects', __('Import', '404-solution'), $html, false, $abj404view->getCardIcon('upload')); |
| 606 |
|
| 607 |
// Purge Card |
| 608 |
$url = "?page=" . ABJ404_PP . "&subpage=abj404_tools"; |
| 609 |
$link = wp_nonce_url($url, "abj404_purgeRedirects"); |
| 610 |
$html = ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/html/toolsPurgeForm.html"); |
| 611 |
$html = $this->f->str_replace('{toolsPurgeFormActionLink}', $link, $html); |
| 612 |
$html = $this->f->doNormalReplacements($html); |
| 613 |
$abj404view->echoOptionsSection('tools-purge', 'abj404-purgeRedirects', __('Purge Options', '404-solution'), $html, false, $abj404view->getCardIcon('trash')); |
| 614 |
|
| 615 |
// Cache Management Card |
| 616 |
$ngramLink = wp_nonce_url("?page=" . ABJ404_PP . "&subpage=abj404_tools", "abj404_rebuildNgramCache"); |
| 617 |
$spellingLink = wp_nonce_url("?page=" . ABJ404_PP . "&subpage=abj404_tools", "abj404_clearSpellingCache"); |
| 618 |
$html = ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/html/toolsCacheForm.html"); |
| 619 |
$html = $this->f->str_replace('{toolsNgramCacheFormActionLink}', $ngramLink, $html); |
| 620 |
$html = $this->f->str_replace('{toolsSpellingCacheFormActionLink}', $spellingLink, $html); |
| 621 |
$html = $this->f->doNormalReplacements($html); |
| 622 |
$abj404view->echoOptionsSection('tools-cache', 'abj404-cacheTools', __('Cache Management', '404-solution'), $html, false, $abj404view->getCardIcon('database')); |
| 623 |
|
| 624 |
// Diagnostics Card |
| 625 |
$html = $this->getToolsDiagnosticsMarkup(); |
| 626 |
$abj404view->echoOptionsSection('tools-diagnostics', 'abj404-diagnosticsTools', __('Diagnostics', '404-solution'), $html, false, $abj404view->getCardIcon('warning')); |
| 627 |
|
| 628 |
// Etcetera Card |
| 629 |
$link = wp_nonce_url("?page=" . ABJ404_PP . "&subpage=abj404_tools", "abj404_runMaintenance"); |
| 630 |
$link .= '&manually_fired=true'; |
| 631 |
$html = ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/html/toolsEtcForm.html"); |
| 632 |
$html = $this->f->str_replace('{toolsMaintenanceFormActionLink}', $link, $html); |
| 633 |
$html = $this->f->doNormalReplacements($html); |
| 634 |
$abj404view->echoOptionsSection('tools-etc', 'abj404-etcTools', __('Etcetera', '404-solution'), $html, false, $abj404view->getCardIcon('cog')); |
| 635 |
|
| 636 |
// Migrate from Another Plugin Card |
| 637 |
$html = $this->getMigrateFromPluginMarkup(); |
| 638 |
$abj404view->echoOptionsSection('tools-migrate', 'abj404-migrateFromPlugin', __('Migrate from Another Plugin', '404-solution'), $html, false, $abj404view->getCardIcon('upload')); |
| 639 |
|
| 640 |
echo "</div>"; |
| 641 |
echo "</div>"; |
| 642 |
} |
| 643 |
|
| 644 |
/** |
| 645 |
* Build the "Migrate from Another Plugin" card markup. |
| 646 |
* Auto-detects installed redirect plugins and renders a two-step preview+import flow. |
| 647 |
* |
| 648 |
* Step 1: User selects a plugin and clicks "Preview Import" — an AJAX request fetches |
| 649 |
* the count of available redirects without importing anything. |
| 650 |
* Step 2: The count is shown. If N > 0, a "Confirm Import" button submits the real |
| 651 |
* import form. If N = 0, "No redirects found" is shown with a Back button. |
| 652 |
* |
| 653 |
* @return string |
| 654 |
*/ |
| 655 |
private function getMigrateFromPluginMarkup(): string { |
| 656 |
$redirectsRepo = abj_service('redirects_repository'); |
| 657 |
$logger = abj_service('logging'); |
| 658 |
$importer = new ABJ_404_Solution_CrossPluginImporter($redirectsRepo, $logger); |
| 659 |
|
| 660 |
$detected = $importer->detectInstalledPlugins(); |
| 661 |
|
| 662 |
$pluginLabels = array( |
| 663 |
'rankmath' => __('Rank Math', '404-solution'), |
| 664 |
'yoast' => __('Yoast SEO Premium', '404-solution'), |
| 665 |
'aioseo' => __('AIOSEO', '404-solution'), |
| 666 |
'safe-redirect-manager' => __('Safe Redirect Manager', '404-solution'), |
| 667 |
'redirection' => __('Redirection Plugin', '404-solution'), |
| 668 |
); |
| 669 |
|
| 670 |
$availableSources = array(); |
| 671 |
foreach ($detected as $slug => $isAvailable) { |
| 672 |
if ($isAvailable) { |
| 673 |
$availableSources[$slug] = isset($pluginLabels[$slug]) ? $pluginLabels[$slug] : $slug; |
| 674 |
} |
| 675 |
} |
| 676 |
|
| 677 |
$migrateActionUrl = wp_nonce_url( |
| 678 |
'?page=' . ABJ404_PP . '&subpage=abj404_tools', |
| 679 |
'abj404_importFromPlugin' |
| 680 |
); |
| 681 |
|
| 682 |
$html = '<p>'; |
| 683 |
if (empty($availableSources)) { |
| 684 |
$html .= esc_html__('No supported redirect plugins detected on this site.', '404-solution'); |
| 685 |
$html .= '</p>'; |
| 686 |
$html .= '<p>' . esc_html__('Supported plugins: Rank Math, Yoast SEO Premium, AIOSEO, Safe Redirect Manager, Redirection.', '404-solution') . '</p>'; |
| 687 |
return $html; |
| 688 |
} |
| 689 |
|
| 690 |
$detectedNames = array_values($availableSources); |
| 691 |
$html .= esc_html__('Detected redirect plugins:', '404-solution') . ' '; |
| 692 |
$html .= '<strong>' . esc_html(implode(', ', $detectedNames)) . '</strong>'; |
| 693 |
$html .= '</p>'; |
| 694 |
|
| 695 |
$previewNonce = wp_create_nonce('abj404_crossPluginPreview'); |
| 696 |
$ajaxUrl = admin_url('admin-ajax.php'); |
| 697 |
|
| 698 |
// Step 1: source selector + Preview button (visible by default) |
| 699 |
$html .= '<div id="abj404-migrate-step1">'; |
| 700 |
$html .= '<p>'; |
| 701 |
$html .= '<label for="abj404-import-source"><strong>' . esc_html__('Source plugin:', '404-solution') . '</strong></label> '; |
| 702 |
$html .= '<select name="import_source" id="abj404-import-source">'; |
| 703 |
foreach ($availableSources as $slug => $label) { |
| 704 |
$html .= '<option value="' . esc_attr($slug) . '">' . esc_html($label) . '</option>'; |
| 705 |
} |
| 706 |
$html .= '</select>'; |
| 707 |
$html .= '</p>'; |
| 708 |
$html .= '<p>'; |
| 709 |
$html .= '<button type="button" id="abj404-migrate-preview-btn" class="button-secondary">'; |
| 710 |
$html .= esc_html__('Preview Import', '404-solution'); |
| 711 |
$html .= '</button>'; |
| 712 |
$html .= ' <span id="abj404-migrate-preview-spinner" style="display:none;margin-left:6px;" class="spinner is-active"></span>'; |
| 713 |
$html .= '</p>'; |
| 714 |
$html .= '</div>'; |
| 715 |
|
| 716 |
// Step 2: preview result + confirm form (hidden until preview completes) |
| 717 |
$html .= '<div id="abj404-migrate-step2" style="display:none;">'; |
| 718 |
$html .= '<p id="abj404-migrate-preview-msg"></p>'; |
| 719 |
$html .= '<form id="abj404-migrate-confirm-form" method="POST" action="' . esc_url($migrateActionUrl) . '" style="display:none;">'; |
| 720 |
$html .= '<input type="hidden" name="action" value="importFromPlugin">'; |
| 721 |
$html .= '<input type="hidden" name="import_source" id="abj404-migrate-confirm-source" value="">'; |
| 722 |
$html .= '<input type="submit" class="button-primary" value="' . esc_attr__('Confirm Import', '404-solution') . '">'; |
| 723 |
$html .= ' <button type="button" id="abj404-migrate-back-btn" class="button-secondary">'; |
| 724 |
$html .= esc_html__('Back', '404-solution'); |
| 725 |
$html .= '</button>'; |
| 726 |
$html .= '</form>'; |
| 727 |
$html .= '<div id="abj404-migrate-back-noform" style="display:none;">'; |
| 728 |
$html .= '<button type="button" id="abj404-migrate-back-btn2" class="button-secondary">'; |
| 729 |
$html .= esc_html__('Back', '404-solution'); |
| 730 |
$html .= '</button>'; |
| 731 |
$html .= '</div>'; |
| 732 |
$html .= '</div>'; |
| 733 |
|
| 734 |
$html .= '<p><em>' . esc_html__('This will import all active redirects from the selected plugin into 404 Solution. Regex and redirect codes are preserved.', '404-solution') . '</em></p>'; |
| 735 |
|
| 736 |
// Inline JS for the two-step flow |
| 737 |
$html .= '<script>(function() {'; |
| 738 |
$html .= 'var ajaxUrl = ' . json_encode($ajaxUrl) . ';'; |
| 739 |
$html .= 'var nonce = ' . json_encode($previewNonce) . ';'; |
| 740 |
$html .= 'var msgFound = ' . json_encode(__('Found %d redirect(s) from %s — proceed with import?', '404-solution')) . ';'; |
| 741 |
$html .= 'var msgNone = ' . json_encode(__('No redirects found in %s. Nothing to import.', '404-solution')) . ';'; |
| 742 |
$html .= 'var msgError = ' . json_encode(__('Could not fetch preview. Please try again.', '404-solution')) . ';'; |
| 743 |
$html .= 'function showStep1() {'; |
| 744 |
$html .= ' document.getElementById("abj404-migrate-step1").style.display = "";'; |
| 745 |
$html .= ' document.getElementById("abj404-migrate-step2").style.display = "none";'; |
| 746 |
$html .= '}'; |
| 747 |
$html .= 'function showStep2(count, source, label) {'; |
| 748 |
$html .= ' document.getElementById("abj404-migrate-step1").style.display = "none";'; |
| 749 |
$html .= ' document.getElementById("abj404-migrate-step2").style.display = "";'; |
| 750 |
$html .= ' var msgEl = document.getElementById("abj404-migrate-preview-msg");'; |
| 751 |
$html .= ' var form = document.getElementById("abj404-migrate-confirm-form");'; |
| 752 |
$html .= ' var noForm = document.getElementById("abj404-migrate-back-noform");'; |
| 753 |
$html .= ' if (count > 0) {'; |
| 754 |
$html .= ' msgEl.textContent = msgFound.replace("%d", count).replace("%s", label);'; |
| 755 |
$html .= ' document.getElementById("abj404-migrate-confirm-source").value = source;'; |
| 756 |
$html .= ' form.style.display = "";'; |
| 757 |
$html .= ' noForm.style.display = "none";'; |
| 758 |
$html .= ' } else {'; |
| 759 |
$html .= ' msgEl.textContent = msgNone.replace("%s", label);'; |
| 760 |
$html .= ' form.style.display = "none";'; |
| 761 |
$html .= ' noForm.style.display = "";'; |
| 762 |
$html .= ' }'; |
| 763 |
$html .= '}'; |
| 764 |
$html .= 'function showError() {'; |
| 765 |
$html .= ' document.getElementById("abj404-migrate-step1").style.display = "none";'; |
| 766 |
$html .= ' document.getElementById("abj404-migrate-step2").style.display = "";'; |
| 767 |
$html .= ' document.getElementById("abj404-migrate-preview-msg").textContent = msgError;'; |
| 768 |
$html .= ' document.getElementById("abj404-migrate-confirm-form").style.display = "none";'; |
| 769 |
$html .= ' document.getElementById("abj404-migrate-back-noform").style.display = "";'; |
| 770 |
$html .= '}'; |
| 771 |
$html .= 'document.addEventListener("DOMContentLoaded", function() {'; |
| 772 |
$html .= ' var previewBtn = document.getElementById("abj404-migrate-preview-btn");'; |
| 773 |
$html .= ' var backBtn = document.getElementById("abj404-migrate-back-btn");'; |
| 774 |
$html .= ' var backBtn2 = document.getElementById("abj404-migrate-back-btn2");'; |
| 775 |
$html .= ' if (previewBtn) {'; |
| 776 |
$html .= ' previewBtn.addEventListener("click", function() {'; |
| 777 |
$html .= ' var select = document.getElementById("abj404-import-source");'; |
| 778 |
$html .= ' var source = select ? select.value : "";'; |
| 779 |
$html .= ' if (!source) return;'; |
| 780 |
$html .= ' var spinner = document.getElementById("abj404-migrate-preview-spinner");'; |
| 781 |
$html .= ' if (spinner) spinner.style.display = "";'; |
| 782 |
$html .= ' previewBtn.disabled = true;'; |
| 783 |
$html .= ' var fd = new FormData();'; |
| 784 |
$html .= ' fd.append("action", "abj404_crossPluginPreview");'; |
| 785 |
$html .= ' fd.append("nonce", nonce);'; |
| 786 |
$html .= ' fd.append("import_source", source);'; |
| 787 |
$html .= ' fetch(ajaxUrl, { method: "POST", body: fd, credentials: "same-origin" })'; |
| 788 |
$html .= ' .then(function(r) { return r.json(); })'; |
| 789 |
$html .= ' .then(function(resp) {'; |
| 790 |
$html .= ' if (spinner) spinner.style.display = "none";'; |
| 791 |
$html .= ' previewBtn.disabled = false;'; |
| 792 |
$html .= ' if (resp && resp.success && resp.data) {'; |
| 793 |
$html .= ' showStep2(parseInt(resp.data.count, 10) || 0, resp.data.source, resp.data.label);'; |
| 794 |
$html .= ' } else { showError(); }'; |
| 795 |
$html .= ' })'; |
| 796 |
$html .= ' .catch(function() {'; |
| 797 |
$html .= ' if (spinner) spinner.style.display = "none";'; |
| 798 |
$html .= ' previewBtn.disabled = false;'; |
| 799 |
$html .= ' showError();'; |
| 800 |
$html .= ' });'; |
| 801 |
$html .= ' });'; |
| 802 |
$html .= ' }'; |
| 803 |
$html .= ' if (backBtn) { backBtn.addEventListener("click", function() { showStep1(); }); }'; |
| 804 |
$html .= ' if (backBtn2) { backBtn2.addEventListener("click", function() { showStep1(); }); }'; |
| 805 |
$html .= '});'; |
| 806 |
$html .= '})();</script>'; |
| 807 |
|
| 808 |
return $html; |
| 809 |
} |
| 810 |
|
| 811 |
/** |
| 812 |
* Build compact diagnostics markup for the Tools page. |
| 813 |
* This is intentionally read-only and lightweight. |
| 814 |
* |
| 815 |
* @return string |
| 816 |
*/ |
| 817 |
private function getToolsDiagnosticsMarkup() { |
| 818 |
$rows = $this->getToolsDiagnosticsRows(); |
| 819 |
$html = '<div class="abj404-diagnostics-summary">'; |
| 820 |
$html .= '<p>' . esc_html__('Quick environment checks for troubleshooting and support.', '404-solution') . '</p>'; |
| 821 |
$html .= '<table class="widefat striped"><tbody>'; |
| 822 |
|
| 823 |
foreach ($rows as $row) { |
| 824 |
$label = array_key_exists('label', $row) ? $row['label'] : ''; |
| 825 |
$value = array_key_exists('value', $row) ? $row['value'] : ''; |
| 826 |
$valueHtml = array_key_exists('value_html', $row) ? $row['value_html'] : ''; |
| 827 |
$status = array_key_exists('status', $row) ? $row['status'] : 'info'; |
| 828 |
$statusLabel = ($status === 'ok') ? __('OK', '404-solution') : (($status === 'warn') ? __('Warning', '404-solution') : __('Info', '404-solution')); |
| 829 |
$statusClass = ($status === 'ok') ? 'abj404-pill-success' : (($status === 'warn') ? 'abj404-pill-warning' : 'abj404-pill-info'); |
| 830 |
|
| 831 |
$html .= '<tr>'; |
| 832 |
$html .= '<td><strong>' . esc_html($label) . '</strong></td>'; |
| 833 |
if ($valueHtml !== '') { |
| 834 |
$html .= '<td>' . wp_kses_post($valueHtml) . '</td>'; |
| 835 |
} else { |
| 836 |
$html .= '<td>' . esc_html($value) . '</td>'; |
| 837 |
} |
| 838 |
$html .= '<td><span class="abj404-status-pill ' . esc_attr($statusClass) . '">' . esc_html($statusLabel) . '</span></td>'; |
| 839 |
$html .= '</tr>'; |
| 840 |
} |
| 841 |
|
| 842 |
$html .= '</tbody></table>'; |
| 843 |
$html .= '</div>'; |
| 844 |
|
| 845 |
return $html; |
| 846 |
} |
| 847 |
|
| 848 |
/** |
| 849 |
* Collect diagnostics fields displayed on the Tools page. |
| 850 |
* |
| 851 |
* @return array<int, array<string, string>> |
| 852 |
*/ |
| 853 |
private function getToolsDiagnosticsRows() { |
| 854 |
$wpVersion = get_bloginfo('version'); |
| 855 |
if (!is_string($wpVersion) || trim($wpVersion) === '') { |
| 856 |
$wpVersion = __('Unknown', '404-solution'); |
| 857 |
} |
| 858 |
|
| 859 |
$uploadDir = ''; |
| 860 |
if (function_exists('abj404_getUploadsDir')) { |
| 861 |
$uploadDir = (string)abj404_getUploadsDir(); |
| 862 |
} |
| 863 |
$uploadDirReadable = ($uploadDir !== '' && is_dir($uploadDir)); |
| 864 |
$uploadDirWritable = ($uploadDir !== '' && is_writable($uploadDir)); |
| 865 |
|
| 866 |
$rows = array(); |
| 867 |
$pluginVersion = defined('ABJ404_VERSION') ? ABJ404_VERSION : __('Unknown', '404-solution'); |
| 868 |
$rows[] = array('label' => __('Plugin Version', '404-solution'), 'value' => $pluginVersion, 'status' => 'info'); |
| 869 |
$rows[] = array('label' => __('WordPress Version', '404-solution'), 'value' => $wpVersion, 'status' => 'info'); |
| 870 |
$rows[] = array('label' => __('PHP Version', '404-solution'), 'value' => PHP_VERSION, 'status' => 'info'); |
| 871 |
$rows[] = array( |
| 872 |
'label' => __('Uploads Directory', '404-solution'), |
| 873 |
'value' => ($uploadDir !== '') ? $uploadDir : __('Not available', '404-solution'), |
| 874 |
'status' => $uploadDirReadable ? 'ok' : 'warn', |
| 875 |
); |
| 876 |
$rows[] = array( |
| 877 |
'label' => __('Uploads Writable', '404-solution'), |
| 878 |
'value' => $uploadDirWritable ? __('Yes', '404-solution') : __('No', '404-solution'), |
| 879 |
'status' => $uploadDirWritable ? 'ok' : 'warn', |
| 880 |
); |
| 881 |
$rows[] = array( |
| 882 |
'label' => __('mbstring Extension', '404-solution'), |
| 883 |
'value' => extension_loaded('mbstring') ? __('Loaded', '404-solution') : __('Missing', '404-solution'), |
| 884 |
'status' => extension_loaded('mbstring') ? 'ok' : 'warn', |
| 885 |
); |
| 886 |
$rows[] = array( |
| 887 |
'label' => __('ZipArchive Support', '404-solution'), |
| 888 |
'value' => class_exists('ZipArchive') ? __('Available', '404-solution') : __('Missing', '404-solution'), |
| 889 |
'status' => class_exists('ZipArchive') ? 'ok' : 'warn', |
| 890 |
); |
| 891 |
$rows[] = array( |
| 892 |
'label' => __('WP_DEBUG', '404-solution'), |
| 893 |
'value' => (defined('WP_DEBUG') && WP_DEBUG) ? __('Enabled', '404-solution') : __('Disabled', '404-solution'), |
| 894 |
'status' => 'info', |
| 895 |
); |
| 896 |
if (function_exists('abj404_is_local_debug_host') && function_exists('abj404_get_simulated_db_latency_ms') && |
| 897 |
abj404_is_local_debug_host()) { |
| 898 |
$latencyMs = absint(abj404_get_simulated_db_latency_ms()); |
| 899 |
$latencyUrls = array( |
| 900 |
250 => wp_nonce_url(admin_url('options-general.php?page=' . ABJ404_PP . '&subpage=abj404_tools&abj404_set_sim_db_ms=250'), 'abj404_set_sim_db_ms'), |
| 901 |
500 => wp_nonce_url(admin_url('options-general.php?page=' . ABJ404_PP . '&subpage=abj404_tools&abj404_set_sim_db_ms=500'), 'abj404_set_sim_db_ms'), |
| 902 |
900 => wp_nonce_url(admin_url('options-general.php?page=' . ABJ404_PP . '&subpage=abj404_tools&abj404_set_sim_db_ms=900'), 'abj404_set_sim_db_ms'), |
| 903 |
0 => wp_nonce_url(admin_url('options-general.php?page=' . ABJ404_PP . '&subpage=abj404_tools&abj404_set_sim_db_ms=0'), 'abj404_set_sim_db_ms'), |
| 904 |
); |
| 905 |
$controls = '<a href="' . esc_url($latencyUrls[250]) . '">' . esc_html(__('250ms', '404-solution')) . '</a>' |
| 906 |
. ' | <a href="' . esc_url($latencyUrls[500]) . '">' . esc_html(__('500ms', '404-solution')) . '</a>' |
| 907 |
. ' | <a href="' . esc_url($latencyUrls[900]) . '">' . esc_html(__('900ms', '404-solution')) . '</a>' |
| 908 |
. ' | <a href="' . esc_url($latencyUrls[0]) . '">' . esc_html(__('Disable', '404-solution')) . '</a>'; |
| 909 |
$rows[] = array( |
| 910 |
'label' => __('Simulated DB Latency', '404-solution'), |
| 911 |
'value' => ($latencyMs > 0) |
| 912 |
? sprintf(__('ON (%d ms per plugin query)', '404-solution'), $latencyMs) |
| 913 |
: __('OFF', '404-solution'), |
| 914 |
'value_html' => '<div>' . esc_html(($latencyMs > 0) |
| 915 |
? sprintf(__('ON (%d ms per plugin query)', '404-solution'), $latencyMs) |
| 916 |
: __('OFF', '404-solution')) . '</div><div>' . $controls . '</div>', |
| 917 |
'status' => ($latencyMs > 0) ? 'warn' : 'info', |
| 918 |
); |
| 919 |
} |
| 920 |
|
| 921 |
return $rows; |
| 922 |
} |
| 923 |
|
| 924 |
|
| 925 |
} |
| 926 |
|