The step card's save form wrapped the entire card, including the autocheck 'Run checks' button and per-step extras like the email 'Send' form — those are also <form> elements. Nested forms are invalid HTML; browsers silently drop the outer form's submit when they see an inner one. So clicking 'Save step' was effectively a no-op: the POST to wph_save_step never reached the server, and the status reverted to 'not started' on reload. The bug had been there since the autocheck panel was introduced in v0.3 of the healthcheck. Restructured the step card as a <div> wrapper containing three siblings: (1) static content (title, blurb, sub-items, watch-outs, escalation), (2) the autocheck panel + step render_extra (each with their own forms), (3) the save form. No nesting anywhere. Verified end-to-end on testsite: step status and notes now persist across page loads. The email step (which has 3 sibling forms — refresh checks, send test, save) also renders without nesting.
651 lines
32 KiB
PHP
651 lines
32 KiB
PHP
<?php
|
|
if (!defined('ABSPATH')) exit;
|
|
|
|
add_action('admin_menu', 'wph_register_menu');
|
|
add_action('admin_post_wph_start', 'wph_handle_start');
|
|
add_action('admin_post_wph_save_step', 'wph_handle_save_step');
|
|
add_action('admin_post_wph_finish', 'wph_handle_finish');
|
|
add_action('admin_post_wph_discard', 'wph_handle_discard');
|
|
add_action('admin_post_wph_download_report', 'wph_handle_download_report');
|
|
add_action('admin_post_wph_refresh_checks', 'wph_handle_refresh_checks');
|
|
add_action('admin_post_wph_download_html', 'wph_handle_download_html');
|
|
add_action('admin_post_wph_email_report', 'wph_handle_email_report');
|
|
add_action('admin_post_wph_step_action', 'wph_handle_step_action');
|
|
add_action('admin_post_wph_recovery_install', 'wph_handle_recovery_install');
|
|
add_action('admin_post_wph_save_settings', 'wph_handle_save_settings');
|
|
add_action('admin_enqueue_scripts', 'wph_enqueue_assets');
|
|
|
|
function wph_register_menu(): void {
|
|
add_management_page(
|
|
'Site Healthcheck',
|
|
'Site Healthcheck',
|
|
'manage_options',
|
|
'site-healthcheck',
|
|
'wph_render_admin_page'
|
|
);
|
|
add_submenu_page(
|
|
null, // hidden — reachable via direct URL
|
|
'Site Healthcheck Settings',
|
|
'Site Healthcheck Settings',
|
|
'manage_options',
|
|
'site-healthcheck-settings',
|
|
'wph_render_settings_page'
|
|
);
|
|
}
|
|
|
|
function wph_enqueue_assets($hook): void {
|
|
if ($hook !== 'tools_page_site-healthcheck') return;
|
|
wp_register_style('wph-admin', false);
|
|
wp_enqueue_style('wph-admin');
|
|
wp_add_inline_style('wph-admin', wph_inline_css());
|
|
}
|
|
|
|
function wph_inline_css(): string {
|
|
return '
|
|
.wph-card { background:#fff; border:1px solid #c3c4c7; border-radius:6px; padding:1rem 1.25rem; margin-bottom:1rem; }
|
|
.wph-card h2 { margin-top:0; }
|
|
.wph-step-status { display:inline-block; padding:.1rem .55rem; border-radius:10px; font-size:11px; font-weight:600; text-transform:uppercase; letter-spacing:.04em; }
|
|
.wph-status-not_started { background:#f0f0f1; color:#646970; }
|
|
.wph-status-done { background:#def7e3; color:#155724; }
|
|
.wph-status-skipped { background:#fff3cd; color:#856404; }
|
|
.wph-status-blocked { background:#fbeae8; color:#721c24; }
|
|
.wph-status-n_a { background:#e2e3e5; color:#41464b; }
|
|
.wph-step { padding:1rem 1.25rem; border:1px solid #dcdcde; border-radius:6px; margin-bottom:.75rem; background:#fff; }
|
|
.wph-step header { display:flex; justify-content:space-between; align-items:center; gap:1rem; margin-bottom:.5rem; }
|
|
.wph-step header h2 { margin:0; font-size:1.1rem; }
|
|
.wph-sub-items { margin:.5rem 0 .75rem 1.25rem; padding:0; }
|
|
.wph-sub-items li { margin:.15rem 0; }
|
|
.wph-watch-outs { background:#fff8e1; border-left:3px solid #f5b800; padding:.4rem .8rem; margin:.5rem 0; font-size:.92em; }
|
|
.wph-watch-outs strong { display:block; margin-bottom:.2rem; }
|
|
.wph-escalation { background:#fbeae8; border-left:3px solid #c0392b; padding:.4rem .8rem; margin:.5rem 0; font-weight:500; }
|
|
.wph-step textarea { width:100%; min-height:60px; }
|
|
.wph-progress { font-weight:600; }
|
|
.wph-ok { color:#155724; }
|
|
.wph-warn { color:#856404; }
|
|
.wph-bad { color:#721c24; }
|
|
.wph-bootstrap-panel { padding:.6rem 1rem; background:#f6f7f7; border:1px solid #dcdcde; border-radius:6px; margin-bottom:.75rem; }
|
|
.wph-bootstrap-panel h3 { margin:0 0 .35rem; font-size:1rem; }
|
|
.wph-actions { display:flex; gap:.5rem; align-items:center; margin-top:.4rem; }
|
|
.wph-autocheck { background:#f6f7f7; border:1px solid #dcdcde; border-radius:6px; padding:.6rem 1rem; margin:.6rem 0; }
|
|
.wph-autocheck header { display:flex; justify-content:space-between; align-items:center; margin:0 0 .4rem; }
|
|
.wph-autocheck header h3 { margin:0; font-size:.95rem; }
|
|
.wph-autocheck table { width:100%; border-collapse:collapse; }
|
|
.wph-autocheck td { padding:.25rem .4rem; vertical-align:top; border-bottom:1px solid #f0f0f1; }
|
|
.wph-autocheck tr:last-child td { border-bottom:0; }
|
|
.wph-autocheck .lvl { width:1.4rem; text-align:center; font-weight:600; }
|
|
.wph-autocheck .lvl-ok { color:#1a8917; }
|
|
.wph-autocheck .lvl-warn { color:#b07a00; }
|
|
.wph-autocheck .lvl-bad { color:#c0392b; }
|
|
.wph-autocheck .lvl-info { color:#646970; }
|
|
.wph-autocheck .label { font-weight:600; }
|
|
.wph-autocheck .value { color:#1d1d1f; }
|
|
.wph-autocheck .detail { color:#646970; font-size:.9em; }
|
|
.wph-checked-at { color:#646970; font-size:.85em; }
|
|
.wph-layout { display:grid; grid-template-columns: 220px 1fr; gap:1rem; }
|
|
.wph-sidebar { position:sticky; top:36px; align-self:start; max-height: calc(100vh - 60px); overflow:auto; }
|
|
.wph-sidebar .wph-card { padding:.75rem 1rem; }
|
|
.wph-sidebar h3 { margin:0 0 .4rem; font-size:.9rem; text-transform:uppercase; letter-spacing:.04em; color:#646970; }
|
|
.wph-sidebar ol { margin:0; padding:0; list-style:none; }
|
|
.wph-sidebar li { padding:.18rem 0; }
|
|
.wph-sidebar a { text-decoration:none; }
|
|
.wph-sidebar .dot { display:inline-block; width:.75rem; height:.75rem; border-radius:50%; margin-right:.45rem; background:#dcdcde; vertical-align:middle; box-shadow:0 0 0 1px rgba(0,0,0,.08) inset; }
|
|
.wph-sidebar .dot-done { background:#16a34a; box-shadow:0 0 0 1px rgba(0,0,0,.15) inset; }
|
|
.wph-sidebar .dot-skipped { background:#f59e0b; }
|
|
.wph-sidebar .dot-blocked { background:#dc2626; }
|
|
.wph-sidebar .dot-n_a { background:#9ca3af; }
|
|
.wph-sidebar li:has(.dot-done) a { color:#15803d; }
|
|
.wph-diff { background:#eef4fb; border:1px solid #cfe0f3; padding:.6rem 1rem; border-radius:6px; margin:.5rem 0; font-size:.9em; }
|
|
.wph-diff strong { display:inline-block; margin-right:.4rem; }
|
|
.wph-diff .delta-new { color:#c0392b; }
|
|
.wph-diff .delta-resolved { color:#1a8917; }
|
|
.wph-diff .delta-changed { color:#b07a00; }
|
|
@media (max-width: 980px) { .wph-layout { grid-template-columns: 1fr; } .wph-sidebar { position: static; max-height: none; } }
|
|
';
|
|
}
|
|
|
|
function wph_render_admin_page(): void {
|
|
if (!current_user_can('manage_options')) wp_die('Forbidden');
|
|
|
|
$session = WPH_Session::current();
|
|
|
|
echo '<div class="wrap">';
|
|
echo '<h1>Site Healthcheck</h1>';
|
|
|
|
if ($msg = get_transient('wph_install_message')) {
|
|
delete_transient('wph_install_message');
|
|
echo '<div class="notice notice-success is-dismissible"><p>' . esc_html($msg) . '</p></div>';
|
|
}
|
|
|
|
if (!$session) {
|
|
wph_render_start_panel();
|
|
echo '</div>';
|
|
return;
|
|
}
|
|
|
|
if ($session->is_finished()) {
|
|
wph_render_finished_panel($session);
|
|
echo '</div>';
|
|
return;
|
|
}
|
|
|
|
wph_render_active_session($session);
|
|
echo '</div>';
|
|
}
|
|
|
|
function wph_render_start_panel(): void {
|
|
?>
|
|
<div class="wph-card">
|
|
<h2>Start a healthcheck</h2>
|
|
<p>This will create a new in-progress session for <code><?php echo esc_html(get_site_url()); ?></code>. One session per site at a time.</p>
|
|
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
|
|
<?php wp_nonce_field('wph_start'); ?>
|
|
<input type="hidden" name="action" value="wph_start">
|
|
<button class="button button-primary">Start new healthcheck</button>
|
|
</form>
|
|
</div>
|
|
<?php
|
|
WPH_Recovery_Bootstrap::render_status();
|
|
}
|
|
|
|
function wph_render_active_session(WPH_Session $session): void {
|
|
$progress = $session->progress();
|
|
$tech = get_userdata($session->technician_id());
|
|
?>
|
|
<div class="wph-card">
|
|
<p>
|
|
<strong>Session:</strong> <code><?php echo esc_html($session->id()); ?></code> ·
|
|
<strong>Started:</strong> <?php echo esc_html(date('Y-m-d H:i', $session->started_at())); ?> ·
|
|
<strong>Technician:</strong> <?php echo esc_html($tech ? $tech->display_name : '#' . $session->technician_id()); ?> ·
|
|
<span class="wph-progress"><?php echo (int) $progress['done']; ?> / <?php echo (int) $progress['total']; ?> steps</span>
|
|
</p>
|
|
<p>WP <code><?php echo esc_html($session->wp_version()); ?></code> · PHP <code><?php echo esc_html($session->php_version()); ?></code> · Site <code><?php echo esc_html($session->site_url()); ?></code></p>
|
|
<div class="wph-actions">
|
|
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" onsubmit="return confirm('Mark this healthcheck as finished?');" style="display:inline">
|
|
<?php wp_nonce_field('wph_finish'); ?>
|
|
<input type="hidden" name="action" value="wph_finish">
|
|
<button class="button button-primary">Finish & generate report</button>
|
|
</form>
|
|
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" onsubmit="return confirm('Discard this in-progress session? All notes will be lost.');" style="display:inline">
|
|
<?php wp_nonce_field('wph_discard'); ?>
|
|
<input type="hidden" name="action" value="wph_discard">
|
|
<button class="button button-link-delete">Discard</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<?php
|
|
|
|
WPH_Recovery_Bootstrap::render_status();
|
|
wph_render_blocked_summary($session);
|
|
wph_render_diff_summary($session);
|
|
|
|
echo '<div class="wph-layout">';
|
|
wph_render_sidebar($session);
|
|
echo '<div>';
|
|
foreach (WPH_Steps::instance()->all() as $step) {
|
|
wph_render_step_card($session, $step);
|
|
}
|
|
echo '</div></div>';
|
|
}
|
|
|
|
function wph_render_sidebar(WPH_Session $session): void {
|
|
echo '<aside class="wph-sidebar"><div class="wph-card"><h3>Steps</h3><ol>';
|
|
foreach (WPH_Steps::instance()->all() as $step) {
|
|
$state = $session->step_state($step->id());
|
|
$cls = 'dot-' . $state['status'];
|
|
printf(
|
|
'<li><span class="dot %s"></span><a href="#step-%s">%s</a></li>',
|
|
esc_attr($cls),
|
|
esc_attr($step->id()),
|
|
esc_html($step->title())
|
|
);
|
|
}
|
|
echo '</ol></div></aside>';
|
|
}
|
|
|
|
function wph_render_blocked_summary(WPH_Session $session): void {
|
|
$blocked = [];
|
|
foreach (WPH_Steps::instance()->all() as $step) {
|
|
$state = $session->step_state($step->id());
|
|
if ($state['status'] === WPH_Session::STATUS_BLOCKED) {
|
|
$blocked[] = ['step' => $step, 'state' => $state];
|
|
}
|
|
}
|
|
if (!$blocked) return;
|
|
echo '<div class="wph-card" style="border-left:4px solid #c0392b">';
|
|
echo '<h2 style="color:#721c24">Stop & escalate</h2>';
|
|
echo '<p>The following steps are blocked. Resolve or escalate before continuing:</p>';
|
|
echo '<ul style="margin-left:1.25rem">';
|
|
foreach ($blocked as $b) {
|
|
$esc = $b['step']->escalation();
|
|
echo '<li><a href="#step-' . esc_attr($b['step']->id()) . '"><strong>' . esc_html($b['step']->title()) . '</strong></a>';
|
|
if ($esc) echo ' — ' . esc_html($esc);
|
|
if (!empty($b['state']['notes'])) echo '<br><em>' . esc_html($b['state']['notes']) . '</em>';
|
|
echo '</li>';
|
|
}
|
|
echo '</ul></div>';
|
|
}
|
|
|
|
function wph_render_diff_summary(WPH_Session $session): void {
|
|
$prev = WPH_Session::previous();
|
|
if (!$prev) return;
|
|
|
|
// Aggregate findings by step+id from each session
|
|
$current_idx = [];
|
|
foreach (WPH_Steps::instance()->all() as $step) {
|
|
$r = $session->get_autocheck($step->id());
|
|
if (!$r) continue;
|
|
foreach ($r['findings'] as $f) {
|
|
$current_idx[$step->id() . '|' . $f['id']] = $f;
|
|
}
|
|
}
|
|
$prev_idx = [];
|
|
foreach (WPH_Steps::instance()->all() as $step) {
|
|
$r = $prev->get_autocheck($step->id());
|
|
if (!$r) continue;
|
|
foreach ($r['findings'] as $f) {
|
|
$prev_idx[$step->id() . '|' . $f['id']] = $f;
|
|
}
|
|
}
|
|
$new = $resolved = $changed = [];
|
|
foreach ($current_idx as $k => $f) {
|
|
if (!isset($prev_idx[$k])) {
|
|
if (in_array($f['level'], ['warn', 'bad'], true)) $new[] = $f;
|
|
} elseif ($prev_idx[$k]['level'] !== $f['level'] || $prev_idx[$k]['value'] !== $f['value']) {
|
|
$changed[] = ['was' => $prev_idx[$k], 'now' => $f];
|
|
}
|
|
}
|
|
foreach ($prev_idx as $k => $f) {
|
|
if (!isset($current_idx[$k]) && in_array($f['level'], ['warn', 'bad'], true)) {
|
|
$resolved[] = $f;
|
|
}
|
|
}
|
|
if (!$new && !$resolved && !$changed) return;
|
|
echo '<div class="wph-diff"><strong>Δ vs. previous session</strong> (finished ' . esc_html(date('Y-m-d', (int) $prev->finished_at())) . ')';
|
|
if ($new) echo ' · <span class="delta-new">' . count($new) . ' new issue(s)</span>';
|
|
if ($resolved) echo ' · <span class="delta-resolved">' . count($resolved) . ' resolved</span>';
|
|
if ($changed) echo ' · <span class="delta-changed">' . count($changed) . ' changed</span>';
|
|
echo '</div>';
|
|
}
|
|
|
|
function wph_render_step_card(WPH_Session $session, WPH_Step $step): void {
|
|
$state = $session->step_state($step->id());
|
|
$status = $state['status'];
|
|
$notes = $state['notes'];
|
|
?>
|
|
<div class="wph-step" id="step-<?php echo esc_attr($step->id()); ?>">
|
|
<header>
|
|
<h2><?php echo esc_html($step->title()); ?></h2>
|
|
<span class="wph-step-status wph-status-<?php echo esc_attr($status); ?>"><?php echo esc_html(str_replace('_', ' ', $status)); ?></span>
|
|
</header>
|
|
<?php if ($blurb = $step->blurb()): ?>
|
|
<p><?php echo esc_html($blurb); ?></p>
|
|
<?php endif; ?>
|
|
<?php if ($items = $step->sub_items()): ?>
|
|
<ul class="wph-sub-items">
|
|
<?php foreach ($items as $item): ?>
|
|
<li><?php echo esc_html($item); ?></li>
|
|
<?php endforeach; ?>
|
|
</ul>
|
|
<?php endif; ?>
|
|
<?php if ($watch = $step->watch_outs()): ?>
|
|
<div class="wph-watch-outs">
|
|
<strong>Watch out for:</strong>
|
|
<ul style="margin:.2rem 0 0 1rem;">
|
|
<?php foreach ($watch as $w): ?>
|
|
<li><?php echo esc_html($w); ?></li>
|
|
<?php endforeach; ?>
|
|
</ul>
|
|
</div>
|
|
<?php endif; ?>
|
|
<?php if ($status === WPH_Session::STATUS_BLOCKED && ($esc = $step->escalation())): ?>
|
|
<div class="wph-escalation"><?php echo esc_html($esc); ?></div>
|
|
<?php endif; ?>
|
|
|
|
<?php
|
|
// Inner forms (autocheck refresh, step-specific extras like email send)
|
|
// are rendered as siblings of the save form — never nested. Nested forms
|
|
// are invalid HTML; browsers drop the outer form's submit silently.
|
|
wph_render_autocheck($session, $step);
|
|
$step->render_extra($session->data());
|
|
?>
|
|
|
|
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" class="wph-step-save">
|
|
<?php wp_nonce_field('wph_save_step_' . $step->id()); ?>
|
|
<input type="hidden" name="action" value="wph_save_step">
|
|
<input type="hidden" name="step" value="<?php echo esc_attr($step->id()); ?>">
|
|
<p>
|
|
<label>
|
|
<strong>Status:</strong>
|
|
<select name="status">
|
|
<?php foreach (WPH_Session::VALID_STATUSES as $s): ?>
|
|
<option value="<?php echo esc_attr($s); ?>" <?php selected($status, $s); ?>><?php echo esc_html(str_replace('_', ' ', $s)); ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</label>
|
|
</p>
|
|
<p>
|
|
<label><strong>Notes:</strong></label>
|
|
<textarea name="notes" placeholder="What did you check, find, fix, or flag?"><?php echo esc_textarea($notes); ?></textarea>
|
|
</p>
|
|
<p>
|
|
<button class="button button-primary">Save step</button>
|
|
<?php if ($state['updated_at']): ?>
|
|
<span class="description">Last saved <?php echo esc_html(human_time_diff($state['updated_at'], time())); ?> ago</span>
|
|
<?php endif; ?>
|
|
</p>
|
|
</form>
|
|
</div>
|
|
<?php
|
|
}
|
|
|
|
function wph_render_finished_panel(WPH_Session $session): void {
|
|
$report = wph_build_markdown_report($session);
|
|
$admin_email = get_option('admin_email');
|
|
?>
|
|
<div class="wph-card">
|
|
<h2>Healthcheck finished</h2>
|
|
<p>Started <?php echo esc_html(date('Y-m-d H:i', $session->started_at())); ?> · Finished <?php echo esc_html(date('Y-m-d H:i', (int) $session->finished_at())); ?></p>
|
|
<div class="wph-actions">
|
|
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" style="display:inline">
|
|
<?php wp_nonce_field('wph_download_report'); ?>
|
|
<input type="hidden" name="action" value="wph_download_report">
|
|
<button class="button button-primary">Download Markdown</button>
|
|
</form>
|
|
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" style="display:inline">
|
|
<?php wp_nonce_field('wph_download_html'); ?>
|
|
<input type="hidden" name="action" value="wph_download_html">
|
|
<button class="button">Download HTML</button>
|
|
</form>
|
|
<button class="button" onclick="navigator.clipboard.writeText(document.getElementById('wph-report-md').textContent);this.textContent='Copied!';setTimeout(()=>this.textContent='Copy Markdown',2000)">Copy Markdown</button>
|
|
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" style="display:inline">
|
|
<?php wp_nonce_field('wph_email_report'); ?>
|
|
<input type="hidden" name="action" value="wph_email_report">
|
|
<input type="email" name="to" placeholder="recipient@example.com" value="<?php echo esc_attr($admin_email); ?>" required style="min-width:14em">
|
|
<button class="button">Email report</button>
|
|
</form>
|
|
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" onsubmit="return confirm('Discard this finished session?');" style="display:inline">
|
|
<?php wp_nonce_field('wph_discard'); ?>
|
|
<input type="hidden" name="action" value="wph_discard">
|
|
<button class="button button-link-delete">Discard</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<div class="wph-card">
|
|
<h2>Report preview</h2>
|
|
<pre id="wph-report-md" style="white-space:pre-wrap;background:#f6f7f7;padding:1rem;border-radius:6px;max-height:30em;overflow:auto"><?php echo esc_html($report); ?></pre>
|
|
</div>
|
|
<?php
|
|
}
|
|
|
|
// --- Handlers ----------------------------------------------------------------
|
|
|
|
function wph_handle_start(): void {
|
|
if (!current_user_can('manage_options')) wp_die('Forbidden');
|
|
check_admin_referer('wph_start');
|
|
WPH_Session::start(get_current_user_id());
|
|
wp_safe_redirect(admin_url('tools.php?page=site-healthcheck'));
|
|
exit;
|
|
}
|
|
|
|
function wph_handle_save_step(): void {
|
|
if (!current_user_can('manage_options')) wp_die('Forbidden');
|
|
$step_id = isset($_POST['step']) ? sanitize_key((string) $_POST['step']) : '';
|
|
check_admin_referer('wph_save_step_' . $step_id);
|
|
$session = WPH_Session::current();
|
|
if (!$session || $session->is_finished()) wp_die('No active session.');
|
|
if (!WPH_Steps::instance()->get($step_id)) wp_die('Unknown step.');
|
|
$status = isset($_POST['status']) ? sanitize_key((string) $_POST['status']) : WPH_Session::STATUS_NOT_STARTED;
|
|
$notes = isset($_POST['notes']) ? wp_unslash((string) $_POST['notes']) : '';
|
|
$session->update_step($step_id, $status, $notes);
|
|
wp_safe_redirect(admin_url('tools.php?page=site-healthcheck#step-' . rawurlencode($step_id)));
|
|
exit;
|
|
}
|
|
|
|
function wph_handle_finish(): void {
|
|
if (!current_user_can('manage_options')) wp_die('Forbidden');
|
|
check_admin_referer('wph_finish');
|
|
$session = WPH_Session::current();
|
|
if (!$session) wp_die('No active session.');
|
|
$session->finish();
|
|
wp_safe_redirect(admin_url('tools.php?page=site-healthcheck'));
|
|
exit;
|
|
}
|
|
|
|
function wph_handle_discard(): void {
|
|
if (!current_user_can('manage_options')) wp_die('Forbidden');
|
|
check_admin_referer('wph_discard');
|
|
WPH_Session::discard();
|
|
wp_safe_redirect(admin_url('tools.php?page=site-healthcheck'));
|
|
exit;
|
|
}
|
|
|
|
function wph_render_autocheck(WPH_Session $session, WPH_Step $step): void {
|
|
if (!$step->has_autocheck()) return;
|
|
$result = $session->get_autocheck($step->id());
|
|
?>
|
|
<div class="wph-autocheck">
|
|
<header>
|
|
<h3>Automated checks</h3>
|
|
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" style="display:inline">
|
|
<?php wp_nonce_field('wph_refresh_checks_' . $step->id()); ?>
|
|
<input type="hidden" name="action" value="wph_refresh_checks">
|
|
<input type="hidden" name="step" value="<?php echo esc_attr($step->id()); ?>">
|
|
<button class="button button-small"><?php echo $result ? 'Refresh' : 'Run checks'; ?></button>
|
|
</form>
|
|
</header>
|
|
<?php if (!$result): ?>
|
|
<p class="description">No automated checks have been run for this step yet.</p>
|
|
<?php else: ?>
|
|
<table>
|
|
<?php foreach ($result['findings'] as $finding):
|
|
$icon = ['ok' => '✓', 'warn' => '⚠', 'bad' => '✗', 'info' => '·'][$finding['level']] ?? '·';
|
|
?>
|
|
<tr>
|
|
<td class="lvl lvl-<?php echo esc_attr($finding['level']); ?>"><?php echo esc_html($icon); ?></td>
|
|
<td class="label"><?php echo esc_html($finding['label']); ?></td>
|
|
<td class="value"><?php echo esc_html($finding['value']); ?></td>
|
|
<td class="detail"><?php echo esc_html($finding['detail']); ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</table>
|
|
<p class="wph-checked-at">Checked <?php echo esc_html(human_time_diff((int) $result['checked_at'], time())); ?> ago (<?php echo esc_html(date('Y-m-d H:i', (int) $result['checked_at'])); ?>)</p>
|
|
<?php endif; ?>
|
|
</div>
|
|
<?php
|
|
}
|
|
|
|
function wph_render_settings_page(): void {
|
|
if (!current_user_can('manage_options')) wp_die('Forbidden');
|
|
$c = WPH_Recovery_Installer::config();
|
|
$o = WPH_Recovery_Installer::config_origin();
|
|
$saved = isset($_GET['wph_saved']);
|
|
?>
|
|
<div class="wrap">
|
|
<h1>Site Healthcheck — Settings</h1>
|
|
<p><a href="<?php echo esc_url(admin_url('tools.php?page=site-healthcheck')); ?>">← Back to healthcheck</a></p>
|
|
|
|
<?php if ($saved): ?><div class="notice notice-success is-dismissible"><p>Saved.</p></div><?php endif; ?>
|
|
|
|
<div class="wph-card">
|
|
<h2>Recovery plugin source (Gitea)</h2>
|
|
<p>One-click install pulls <code>site-recovery</code> from a private Gitea repo. The token needs read access to the repo only — a deploy / read-only PAT is safer than a personal token.</p>
|
|
<p>Each field can be set via a constant in <code>wp-config.php</code> (then it takes precedence and the field below is locked).</p>
|
|
|
|
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
|
|
<?php wp_nonce_field('wph_save_settings'); ?>
|
|
<input type="hidden" name="action" value="wph_save_settings">
|
|
<table class="form-table">
|
|
<tr>
|
|
<th><label for="wph-host">Gitea host</label></th>
|
|
<td>
|
|
<input id="wph-host" type="url" name="host" value="<?php echo esc_attr($c['host']); ?>" class="regular-text" placeholder="https://git.example.com" <?php disabled($o['host']); ?>>
|
|
<?php if ($o['host']): ?><p class="description">Set via <code>WPH_GITEA_HOST</code> constant.</p><?php endif; ?>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<th><label for="wph-owner">Owner</label></th>
|
|
<td>
|
|
<input id="wph-owner" type="text" name="owner" value="<?php echo esc_attr($c['owner']); ?>" class="regular-text" placeholder="steve" <?php disabled($o['owner']); ?>>
|
|
<?php if ($o['owner']): ?><p class="description">Set via <code>WPH_GITEA_OWNER</code> constant.</p><?php endif; ?>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<th><label for="wph-repo">Repo</label></th>
|
|
<td>
|
|
<input id="wph-repo" type="text" name="repo" value="<?php echo esc_attr($c['repo']); ?>" class="regular-text" placeholder="site-recovery" <?php disabled($o['repo']); ?>>
|
|
<?php if ($o['repo']): ?><p class="description">Set via <code>WPH_GITEA_REPO</code> constant.</p><?php endif; ?>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<th><label for="wph-token">Token</label></th>
|
|
<td>
|
|
<input id="wph-token" type="password" name="token" value="<?php echo esc_attr($c['token']); ?>" class="regular-text" autocomplete="new-password" <?php disabled($o['token']); ?>>
|
|
<?php if ($o['token']): ?><p class="description">Set via <code>WPH_GITEA_TOKEN</code> constant.</p>
|
|
<?php else: ?><p class="description">Stored in WP options. Use a read-only deploy token scoped to this repo if possible.</p><?php endif; ?>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
<p><button class="button button-primary">Save settings</button></p>
|
|
</form>
|
|
|
|
<?php if (WPH_Recovery_Installer::is_configured() && !WPH_Recovery_Bootstrap::is_installed()): ?>
|
|
<hr>
|
|
<h3>Install now</h3>
|
|
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" onsubmit="return confirm('Download and install Site Recovery now?');">
|
|
<?php wp_nonce_field('wph_recovery_install'); ?>
|
|
<input type="hidden" name="action" value="wph_recovery_install">
|
|
<button class="button button-primary">Install from gitea (latest)</button>
|
|
</form>
|
|
<?php elseif (WPH_Recovery_Bootstrap::is_installed()): ?>
|
|
<hr>
|
|
<p class="wph-ok">✓ Site Recovery is already installed at <code><?php echo esc_html((string) WPH_Recovery_Bootstrap::plugin_file()); ?></code>.</p>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
<?php
|
|
}
|
|
|
|
function wph_handle_save_settings(): void {
|
|
if (!current_user_can('manage_options')) wp_die('Forbidden');
|
|
check_admin_referer('wph_save_settings');
|
|
WPH_Recovery_Installer::save_options([
|
|
'host' => (string) wp_unslash($_POST['host'] ?? ''),
|
|
'owner' => (string) wp_unslash($_POST['owner'] ?? ''),
|
|
'repo' => (string) wp_unslash($_POST['repo'] ?? ''),
|
|
'token' => (string) wp_unslash($_POST['token'] ?? ''),
|
|
]);
|
|
wp_safe_redirect(admin_url('tools.php?page=site-healthcheck-settings&wph_saved=1'));
|
|
exit;
|
|
}
|
|
|
|
function wph_handle_recovery_install(): void {
|
|
if (!current_user_can('install_plugins') || !current_user_can('activate_plugins')) wp_die('Forbidden');
|
|
check_admin_referer('wph_recovery_install');
|
|
@set_time_limit(120);
|
|
$result = WPH_Recovery_Installer::install_and_activate();
|
|
if (is_wp_error($result)) {
|
|
wp_die('Install failed: ' . esc_html($result->get_error_message()) . ' <p><a href="' . esc_url(admin_url('tools.php?page=site-healthcheck-settings')) . '">Back to settings</a></p>');
|
|
}
|
|
set_transient('wph_install_message', sprintf('Site Recovery installed from %s "%s" and activated.', $result['ref']['type'], $result['ref']['ref']), 60);
|
|
wp_safe_redirect(admin_url('tools.php?page=site-healthcheck'));
|
|
exit;
|
|
}
|
|
|
|
function wph_handle_step_action(): void {
|
|
if (!current_user_can('manage_options')) wp_die('Forbidden');
|
|
$step_id = isset($_POST['step']) ? sanitize_key((string) $_POST['step']) : '';
|
|
$action_name = isset($_POST['step_action']) ? sanitize_key((string) $_POST['step_action']) : '';
|
|
check_admin_referer('wph_step_action_' . $step_id . '_' . $action_name);
|
|
$session = WPH_Session::current();
|
|
if (!$session || $session->is_finished()) wp_die('No active session.');
|
|
$step = WPH_Steps::instance()->get($step_id);
|
|
if (!$step) wp_die('Unknown step.');
|
|
|
|
// Pass POST through unslashed so handlers see the raw user input.
|
|
$input = wp_unslash($_POST);
|
|
@set_time_limit(60);
|
|
$finding = $step->handle_action($action_name, is_array($input) ? $input : []);
|
|
|
|
if (is_array($finding)) {
|
|
// Append (or replace by id) onto this step's stored findings.
|
|
$existing = $session->get_autocheck($step_id);
|
|
$findings = $existing['findings'] ?? [];
|
|
$replaced = false;
|
|
foreach ($findings as $i => $f) {
|
|
if (($f['id'] ?? '') === ($finding['id'] ?? '')) {
|
|
$findings[$i] = $finding;
|
|
$replaced = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!$replaced) $findings[] = $finding;
|
|
$session->set_autocheck($step_id, $findings);
|
|
}
|
|
|
|
wp_safe_redirect(admin_url('tools.php?page=site-healthcheck#step-' . rawurlencode($step_id)));
|
|
exit;
|
|
}
|
|
|
|
function wph_handle_refresh_checks(): void {
|
|
if (!current_user_can('manage_options')) wp_die('Forbidden');
|
|
$step_id = isset($_POST['step']) ? sanitize_key((string) $_POST['step']) : '';
|
|
check_admin_referer('wph_refresh_checks_' . $step_id);
|
|
$session = WPH_Session::current();
|
|
if (!$session || $session->is_finished()) wp_die('No active session.');
|
|
$step = WPH_Steps::instance()->get($step_id);
|
|
if (!$step) wp_die('Unknown step.');
|
|
@set_time_limit(60);
|
|
$findings = $step->autocheck($session->data());
|
|
$session->set_autocheck($step_id, $findings);
|
|
wp_safe_redirect(admin_url('tools.php?page=site-healthcheck#step-' . rawurlencode($step_id)));
|
|
exit;
|
|
}
|
|
|
|
function wph_handle_download_report(): void {
|
|
if (!current_user_can('manage_options')) wp_die('Forbidden');
|
|
check_admin_referer('wph_download_report');
|
|
$session = WPH_Session::current();
|
|
if (!$session) wp_die('No session.');
|
|
wph_stream_report($session, 'md');
|
|
}
|
|
|
|
function wph_handle_download_html(): void {
|
|
if (!current_user_can('manage_options')) wp_die('Forbidden');
|
|
check_admin_referer('wph_download_html');
|
|
$session = WPH_Session::current();
|
|
if (!$session) wp_die('No session.');
|
|
wph_stream_report($session, 'html');
|
|
}
|
|
|
|
function wph_stream_report(WPH_Session $session, string $format): void {
|
|
$host = parse_url(get_site_url(), PHP_URL_HOST) ?: 'site';
|
|
$host = preg_replace('/[^a-z0-9.-]/i', '', (string) $host);
|
|
$stamp = date('Ymd', $session->started_at() ?: time());
|
|
nocache_headers();
|
|
if ($format === 'html') {
|
|
header('Content-Type: text/html; charset=UTF-8');
|
|
header('Content-Disposition: attachment; filename="wph-report-' . $host . '-' . $stamp . '.html"');
|
|
echo wph_build_html_report($session);
|
|
} else {
|
|
header('Content-Type: text/markdown; charset=UTF-8');
|
|
header('Content-Disposition: attachment; filename="wph-report-' . $host . '-' . $stamp . '.md"');
|
|
echo wph_build_markdown_report($session);
|
|
}
|
|
exit;
|
|
}
|
|
|
|
function wph_handle_email_report(): void {
|
|
if (!current_user_can('manage_options')) wp_die('Forbidden');
|
|
check_admin_referer('wph_email_report');
|
|
$session = WPH_Session::current();
|
|
if (!$session) wp_die('No session.');
|
|
$to = isset($_POST['to']) ? sanitize_email((string) wp_unslash($_POST['to'])) : '';
|
|
if (!is_email($to)) wp_die('Bad email address.');
|
|
$host = parse_url(get_site_url(), PHP_URL_HOST) ?: 'site';
|
|
$subject = 'Site Healthcheck — ' . $host . ' — ' . date('Y-m-d', $session->started_at() ?: time());
|
|
$html = wph_build_html_report($session);
|
|
$ok = wp_mail($to, $subject, $html, ['Content-Type: text/html; charset=UTF-8']);
|
|
wp_safe_redirect(admin_url('tools.php?page=site-healthcheck&wph_mail=' . ($ok ? '1' : '0')));
|
|
exit;
|
|
}
|