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.
106 lines
4.1 KiB
PHP
106 lines
4.1 KiB
PHP
<?php
|
||
if (!defined('ABSPATH')) exit;
|
||
|
||
/**
|
||
* Build a Markdown report for a healthcheck session.
|
||
* Per bead hc-5ix.7: pure Markdown, downloadable. No DB-side report storage
|
||
* (plugin may be uninstalled at the end of the engagement).
|
||
*/
|
||
function wph_build_markdown_report(WPH_Session $session): string {
|
||
$tech = get_userdata($session->technician_id());
|
||
$tech_name = $tech ? $tech->display_name : '#' . $session->technician_id();
|
||
|
||
$lines = [];
|
||
$lines[] = '# Site Healthcheck — ' . $session->site_url();
|
||
$lines[] = '';
|
||
$lines[] = '- **Started:** ' . date('Y-m-d H:i', $session->started_at());
|
||
if ($session->is_finished()) {
|
||
$lines[] = '- **Finished:** ' . date('Y-m-d H:i', (int) $session->finished_at());
|
||
$minutes = max(1, (int) round(((int) $session->finished_at() - $session->started_at()) / 60));
|
||
$lines[] = '- **Duration:** ~' . $minutes . ' minute(s)';
|
||
}
|
||
$lines[] = '- **Technician:** ' . $tech_name;
|
||
$lines[] = '- **Site:** ' . $session->site_url();
|
||
$lines[] = '- **WordPress:** ' . $session->wp_version();
|
||
$lines[] = '- **PHP:** ' . $session->php_version();
|
||
$lines[] = '- **Session ID:** ' . $session->id();
|
||
$lines[] = '';
|
||
|
||
// Summary table
|
||
$lines[] = '## Summary';
|
||
$lines[] = '';
|
||
$lines[] = '| Step | Status |';
|
||
$lines[] = '|---|---|';
|
||
foreach (WPH_Steps::instance()->all() as $step) {
|
||
$state = $session->step_state($step->id());
|
||
$lines[] = '| ' . $step->title() . ' | ' . wph_status_label($state['status']) . ' |';
|
||
}
|
||
$lines[] = '';
|
||
|
||
// Per-step detail
|
||
$lines[] = '## Detail';
|
||
$lines[] = '';
|
||
foreach (WPH_Steps::instance()->all() as $step) {
|
||
$state = $session->step_state($step->id());
|
||
$lines[] = '### ' . $step->title();
|
||
$lines[] = '';
|
||
$lines[] = '_Status: ' . wph_status_label($state['status']) . '_';
|
||
if ($state['updated_at']) {
|
||
$lines[] = '_Saved: ' . date('Y-m-d H:i', $state['updated_at']) . '_';
|
||
}
|
||
if ($blurb = $step->blurb()) {
|
||
$lines[] = '';
|
||
$lines[] = $blurb;
|
||
}
|
||
if ($items = $step->sub_items()) {
|
||
$lines[] = '';
|
||
foreach ($items as $item) {
|
||
$lines[] = '- [ ] ' . $item;
|
||
}
|
||
}
|
||
if (!empty($state['notes'])) {
|
||
$lines[] = '';
|
||
$lines[] = '**Notes:**';
|
||
$lines[] = '';
|
||
foreach (preg_split('/\R/', (string) $state['notes']) as $nl) {
|
||
$lines[] = '> ' . $nl;
|
||
}
|
||
}
|
||
|
||
$auto = $session->get_autocheck($step->id());
|
||
if ($auto && !empty($auto['findings'])) {
|
||
$lines[] = '';
|
||
$lines[] = '**Automated checks** (run ' . date('Y-m-d H:i', (int) ($auto['checked_at'] ?? 0)) . '):';
|
||
$lines[] = '';
|
||
foreach ($auto['findings'] as $finding) {
|
||
$icon = ['ok' => '✅', 'warn' => '⚠️', 'bad' => '❌', 'info' => 'ℹ️'][$finding['level']] ?? '·';
|
||
$bits = [$icon, '**' . $finding['label'] . '**'];
|
||
if (!empty($finding['value'])) $bits[] = $finding['value'];
|
||
if (!empty($finding['detail'])) $bits[] = '— ' . $finding['detail'];
|
||
$lines[] = '- ' . implode(' ', $bits);
|
||
}
|
||
}
|
||
if ($state['status'] === WPH_Session::STATUS_BLOCKED && ($esc = $step->escalation())) {
|
||
$lines[] = '';
|
||
$lines[] = '> ⚠ **Escalation:** ' . $esc;
|
||
}
|
||
$lines[] = '';
|
||
}
|
||
|
||
$lines[] = '---';
|
||
$lines[] = '_Generated by Site Healthcheck plugin v' . WPH_VERSION . '_';
|
||
|
||
return implode("\n", $lines) . "\n";
|
||
}
|
||
|
||
function wph_status_label(string $status): string {
|
||
switch ($status) {
|
||
case WPH_Session::STATUS_DONE: return '✅ Done';
|
||
case WPH_Session::STATUS_SKIPPED: return '⏭ Skipped';
|
||
case WPH_Session::STATUS_BLOCKED: return '🛑 Blocked';
|
||
case WPH_Session::STATUS_NA: return '— N/A';
|
||
case WPH_Session::STATUS_NOT_STARTED:
|
||
default: return '◻ Not started';
|
||
}
|
||
}
|