Files
wp-healthcheck/includes/steps/20-environment.php
Steve Hanlon 0d51fc3b59 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.
2026-06-11 16:02:34 +01:00

148 lines
6.4 KiB
PHP

<?php
if (!defined('ABSPATH')) exit;
return new class extends WPH_Step {
public function id(): string { return 'environment'; }
public function title(): string { return 'Step 2 — Environment Check'; }
public function blurb(): string {
return 'Before touching updates, review the hosting environment.';
}
public function sub_items(): array {
return [
'PHP version — check against WordPress requirements and plugin compatibility. Flag if below 8.1. Note if EOL.',
'Disk usage — flag if over 80% used',
'Error logs — check the server error log and WordPress debug log if enabled. Note any recurring errors, 500s, or deprecated function warnings',
'wp-config.php — confirm WP_DEBUG is off on production',
'File permissions — spot check wp-config.php (should be 640 or 600), wp-content (755), uploads (755)',
];
}
public function autocheck(array $session_state): array {
$f = [];
// PHP version + EOL
// EOL dates from php.net (Y-m-d). Conservative — bump as new versions ship.
$eol = [
'7.4' => '2022-11-28',
'8.0' => '2023-11-26',
'8.1' => '2025-12-31',
'8.2' => '2026-12-31',
'8.3' => '2027-12-31',
'8.4' => '2028-12-31',
];
$php = PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION;
if (isset($eol[$php])) {
$is_eol = strtotime($eol[$php]) < time();
$f[] = $this->finding(
'php_version',
$is_eol ? 'bad' : (version_compare($php, '8.1', '<') ? 'warn' : 'ok'),
'PHP version',
PHP_VERSION,
$is_eol ? "EOL since {$eol[$php]} — upgrade urgently." : "Supported until {$eol[$php]}."
);
} else {
$f[] = $this->finding('php_version', 'info', 'PHP version', PHP_VERSION, 'EOL date unknown for this branch.');
}
// WP version vs latest
global $wp_version;
if (!function_exists('get_core_updates')) require_once ABSPATH . 'wp-admin/includes/update.php';
$updates = function_exists('get_core_updates') ? get_core_updates(['dismissed' => true]) : [];
$latest = (!empty($updates) && !empty($updates[0]->current)) ? $updates[0]->current : $wp_version;
$behind = version_compare($wp_version, $latest, '<');
$f[] = $this->finding(
'wp_version',
$behind ? 'warn' : 'ok',
'WordPress version',
$wp_version,
$behind ? "Latest is {$latest} — update available." : 'Up to date.'
);
// Disk usage on ABSPATH
$free = @disk_free_space(ABSPATH);
$total = @disk_total_space(ABSPATH);
if ($free !== false && $total !== false && $total > 0) {
$used_pct = round(($total - $free) / $total * 100, 1);
$level = $used_pct > 90 ? 'bad' : ($used_pct > 80 ? 'warn' : 'ok');
$f[] = $this->finding(
'disk_usage',
$level,
'Disk usage',
$used_pct . '% used',
size_format($total - $free) . ' of ' . size_format($total) . ' (free: ' . size_format($free) . ')'
);
}
// wp-config flags
$flags = [
'WP_DEBUG' => false,
'WP_DEBUG_DISPLAY' => true, // default true; we want false on production
'WP_DEBUG_LOG' => false,
'DISALLOW_FILE_EDIT' => false, // we want this true
'WP_ENVIRONMENT_TYPE'=> 'production',
];
foreach ($flags as $const => $expected_for_prod) {
if (!defined($const)) {
if ($const === 'WP_ENVIRONMENT_TYPE') continue; // optional
$f[] = $this->finding('flag_' . strtolower($const), 'info', $const, 'not defined', 'Default applies.');
continue;
}
$val = constant($const);
if ($const === 'WP_ENVIRONMENT_TYPE') {
$f[] = $this->finding('flag_env_type', 'info', 'WP_ENVIRONMENT_TYPE', (string) $val, '');
continue;
}
$bool = (bool) $val;
$ok = ($const === 'DISALLOW_FILE_EDIT') ? ($bool === true) : ($bool === false);
$level = $ok ? 'ok' : ($const === 'WP_DEBUG_DISPLAY' ? 'bad' : 'warn');
$f[] = $this->finding(
'flag_' . strtolower($const),
$level,
$const,
$bool ? 'true' : 'false',
$ok ? '' : ($const === 'WP_DEBUG_DISPLAY' ? 'Errors are being shown to visitors — turn this off on production.' : 'Recommended: ' . ($expected_for_prod ? 'true' : 'false') . ' on production.')
);
}
// Permissions on key paths
foreach ([
'wp-config.php' => ABSPATH . 'wp-config.php',
'wp-content/' => WP_CONTENT_DIR,
'uploads/' => wp_get_upload_dir()['basedir'] ?? WP_CONTENT_DIR . '/uploads',
] as $label => $path) {
if (!file_exists($path)) continue;
$perms = substr(sprintf('%o', fileperms($path)), -4);
// wp-config: 600 or 640; dirs: 755 (loose check)
$is_cfg = $label === 'wp-config.php';
$ok = $is_cfg ? in_array($perms, ['0600', '0640'], true)
: in_array($perms, ['0755', '0750'], true);
$f[] = $this->finding(
'perm_' . sanitize_key($label),
$ok ? 'ok' : 'warn',
'Permissions: ' . $label,
$perms,
$ok ? '' : ($is_cfg ? 'Recommend 600 or 640.' : 'Recommend 755 or 750.')
);
}
// Error log size (PHP error_log + WP debug.log)
foreach ([
'PHP error_log' => ini_get('error_log'),
'WP debug.log' => WP_CONTENT_DIR . '/debug.log',
] as $label => $path) {
if (!$path || !file_exists($path) || !is_readable($path)) continue;
$size = filesize($path);
$level = $size > 10 * MB_IN_BYTES ? 'warn' : 'info';
$f[] = $this->finding(
'log_' . sanitize_key($label),
$level,
$label,
size_format($size),
$path . ($level === 'warn' ? ' — large file, consider rotating + reviewing tail.' : '')
);
}
return $f;
}
};