Phase 3 v1: autocheck framework + six step automations

Infrastructure:
- WPH_Step::autocheck($session_state) returns an array of findings
  shaped {id, level: ok/warn/bad/info, label, value, detail}.
- WPH_Session stores results keyed by step id (persisted in the option-
  backed session).
- WPH_Step::has_autocheck() reflection check so the UI only renders the
  panel for steps that implement automation.
- 'Run checks' / 'Refresh' button per step, admin-post handler runs
  autocheck() and stashes the result on the session.
- Findings rendered as a coloured table on the step card; included
  verbatim in the Markdown report with status icons.

Step automations implemented:
- Step 1 (Backup): detection of 11 known backup plugins by slug;
  active/inactive state; UpdraftPlus last-backup timestamp.
- Step 2 (Environment): PHP version + EOL, WP version vs latest, disk
  usage, wp-config flags, file perms on wp-config/wp-content/uploads,
  error-log sizes.
- Step 4 (Plugins): WP.org API enrichment with 24h transient cache —
  last_updated, active_installs, abandonment flag, removed-from-repo
  flag, update-available count. Summary line at the top.
- Step 8 (Security): SSL cert expiry via stream_socket_client +
  openssl_x509_parse, administrator audit, xmlrpc reachability, login
  URL hardening detection.
- Step 9 (Database): spam comments, post revisions, autoload size (WP
  6.6+ value handling), top 3 largest tables.
- Step 11 (Small fixes): deactivated-but-installed plugin list,
  homepage alt-text scan.

Smoke-tested on testsite — all six steps return findings with
correctly-classified levels. Report regenerated with automated findings
section.
This commit is contained in:
2026-06-11 16:02:34 +01:00
parent 8de7cad0de
commit 0d51fc3b59
11 changed files with 646 additions and 9 deletions

View File

@@ -7,6 +7,7 @@ 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_enqueue_scripts', 'wph_enqueue_assets');
function wph_register_menu(): void {
@@ -52,6 +53,21 @@ function wph_inline_css(): string {
.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; }
';
}
@@ -164,6 +180,7 @@ function wph_render_step_card(WPH_Session $session, WPH_Step $step): void {
<?php if ($status === WPH_Session::STATUS_BLOCKED && ($esc = $step->escalation())): ?>
<div class="wph-escalation"><?php echo esc_html($esc); ?></div>
<?php endif; ?>
<?php wph_render_autocheck($session, $step); ?>
<p>
<label>
<strong>Status:</strong>
@@ -257,6 +274,56 @@ function wph_handle_discard(): void {
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_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');