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:
@@ -17,4 +17,81 @@ return new class extends WPH_Step {
|
||||
public function escalation(): ?string {
|
||||
return 'If the backup fails or cannot be confirmed, stop. Do not proceed until you have a verified backup.';
|
||||
}
|
||||
|
||||
public function autocheck(array $session_state): array {
|
||||
if (!function_exists('get_plugins')) require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
||||
$plugins = get_plugins();
|
||||
|
||||
// Map known backup plugins: slug → friendly label.
|
||||
$known = [
|
||||
'updraftplus/updraftplus.php' => 'UpdraftPlus',
|
||||
'backwpup/backwpup.php' => 'BackWPup',
|
||||
'backup-backup/backup-backup.php' => 'Backup Migration',
|
||||
'duplicator/duplicator.php' => 'Duplicator',
|
||||
'wpvivid-backuprestore/wpvivid-backuprestore.php' => 'WPvivid',
|
||||
'all-in-one-wp-migration/all-in-one-wp-migration.php' => 'All-in-One WP Migration',
|
||||
'blogvault-real-time-backup/blogvault.php' => 'BlogVault',
|
||||
'jetpack/jetpack.php' => 'Jetpack (VaultPress)',
|
||||
'solid-backups/backupbuddy.php' => 'Solid Backups',
|
||||
'backupbuddy/backupbuddy.php' => 'BackupBuddy',
|
||||
'wp-time-capsule/wp-time-capsule.php' => 'WP Time Capsule',
|
||||
];
|
||||
|
||||
$found = [];
|
||||
foreach ($known as $file => $label) {
|
||||
if (isset($plugins[$file])) {
|
||||
$found[$file] = [
|
||||
'label' => $label,
|
||||
'active' => is_plugin_active($file),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$f = [];
|
||||
if (empty($found)) {
|
||||
$f[] = $this->finding(
|
||||
'no_backup_plugin', 'bad',
|
||||
'No recognised backup plugin installed', '',
|
||||
'None of the common backup plugins were detected. Confirm host-side backups instead, or install one.'
|
||||
);
|
||||
return $f;
|
||||
}
|
||||
|
||||
$any_active = false;
|
||||
foreach ($found as $file => $meta) {
|
||||
if ($meta['active']) $any_active = true;
|
||||
$f[] = $this->finding(
|
||||
'plugin_' . sanitize_key($file),
|
||||
$meta['active'] ? 'ok' : 'warn',
|
||||
$meta['label'],
|
||||
$meta['active'] ? 'active' : 'installed but inactive',
|
||||
''
|
||||
);
|
||||
}
|
||||
if (!$any_active) {
|
||||
$f[] = $this->finding(
|
||||
'no_active', 'bad',
|
||||
'No backup plugin is active', '',
|
||||
'A backup plugin is installed but not active — activate before proceeding.'
|
||||
);
|
||||
}
|
||||
|
||||
// UpdraftPlus — surface last backup timestamp if available.
|
||||
if (isset($found['updraftplus/updraftplus.php'])) {
|
||||
$last = get_option('updraft_last_backup');
|
||||
if (is_array($last) && !empty($last['backup_time'])) {
|
||||
$age_days = floor((time() - (int) $last['backup_time']) / DAY_IN_SECONDS);
|
||||
$level = $age_days > 7 ? 'warn' : 'ok';
|
||||
$f[] = $this->finding(
|
||||
'updraft_last',
|
||||
$level,
|
||||
'UpdraftPlus — last backup',
|
||||
date('Y-m-d H:i', (int) $last['backup_time']),
|
||||
$age_days . ' day(s) ago' . (!empty($last['backup_nonce']) ? '. Nonce ' . substr((string) $last['backup_nonce'], 0, 8) : '')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $f;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -16,4 +16,51 @@ return new class extends WPH_Step {
|
||||
'Clearing accumulated spam comments',
|
||||
];
|
||||
}
|
||||
|
||||
public function autocheck(array $session_state): array {
|
||||
$f = [];
|
||||
|
||||
// Deactivated-but-installed plugins
|
||||
if (!function_exists('get_plugins')) require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
||||
$inactive = [];
|
||||
foreach (get_plugins() as $file => $meta) {
|
||||
if (!is_plugin_active($file)) $inactive[] = ($meta['Name'] ?? $file);
|
||||
}
|
||||
$f[] = $this->finding(
|
||||
'inactive_plugins',
|
||||
count($inactive) > 0 ? 'info' : 'ok',
|
||||
'Deactivated-but-installed plugins',
|
||||
(string) count($inactive),
|
||||
$inactive ? implode(', ', array_slice($inactive, 0, 10)) . (count($inactive) > 10 ? ' …' : '') : ''
|
||||
);
|
||||
|
||||
// Homepage alt-text scan
|
||||
$url = home_url('/');
|
||||
$resp = wp_remote_get($url, ['timeout' => 6]);
|
||||
if (is_wp_error($resp)) {
|
||||
$f[] = $this->finding('homepage_alt', 'warn', 'Homepage alt text', 'fetch failed', $resp->get_error_message());
|
||||
} else {
|
||||
$body = (string) wp_remote_retrieve_body($resp);
|
||||
$imgs = 0;
|
||||
$missing = 0;
|
||||
if (preg_match_all('/<img\b[^>]*>/i', $body, $matches)) {
|
||||
foreach ($matches[0] as $tag) {
|
||||
$imgs++;
|
||||
if (!preg_match('/\salt\s*=\s*"[^"]+"/i', $tag) && !preg_match('/\salt\s*=\s*\'[^\']+\'/i', $tag)) {
|
||||
$missing++;
|
||||
}
|
||||
}
|
||||
}
|
||||
$level = $missing > 0 ? 'warn' : 'ok';
|
||||
$f[] = $this->finding(
|
||||
'homepage_alt',
|
||||
$level,
|
||||
'Homepage alt text',
|
||||
$missing . ' missing of ' . $imgs . ' image(s)',
|
||||
$missing ? 'Empty/missing alt attributes hurt accessibility and SEO.' : ''
|
||||
);
|
||||
}
|
||||
|
||||
return $f;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -16,4 +16,132 @@ return new class extends WPH_Step {
|
||||
'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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -22,4 +22,112 @@ return new class extends WPH_Step {
|
||||
'Plugins showing "Update unavailable" or removed from the WordPress repository — flag immediately, these can indicate abandoned or compromised plugins',
|
||||
];
|
||||
}
|
||||
|
||||
public function autocheck(array $session_state): array {
|
||||
if (!function_exists('get_plugins')) require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
||||
$plugins = get_plugins();
|
||||
$f = [];
|
||||
$abandoned = 0;
|
||||
$removed = 0;
|
||||
$update_avail = 0;
|
||||
|
||||
// Refresh update transient (no-op if recent)
|
||||
if (function_exists('wp_update_plugins')) wp_update_plugins();
|
||||
$updates = get_site_transient('update_plugins');
|
||||
$update_map = isset($updates->response) && is_array($updates->response) ? $updates->response : [];
|
||||
|
||||
foreach ($plugins as $file => $meta) {
|
||||
$slug = dirname($file);
|
||||
if ($slug === '.' || $slug === '') $slug = basename($file, '.php');
|
||||
$info = $this->wp_org_info($slug);
|
||||
$detail_parts = [];
|
||||
$level = 'ok';
|
||||
|
||||
if ($info === 'not_in_repo') {
|
||||
$level = 'warn';
|
||||
$detail_parts[] = 'Not in WP.org repo (could be premium, custom, or removed)';
|
||||
$removed++;
|
||||
} elseif ($info === 'removed') {
|
||||
$level = 'bad';
|
||||
$detail_parts[] = '⚠ Removed from WP.org repo — possible abandonment or compromise';
|
||||
$removed++;
|
||||
} elseif (is_array($info)) {
|
||||
if (!empty($info['last_updated'])) {
|
||||
$age_days = floor((time() - strtotime($info['last_updated'])) / DAY_IN_SECONDS);
|
||||
$detail_parts[] = 'Last release ' . $age_days . ' day(s) ago (' . $info['last_updated'] . ')';
|
||||
if ($age_days > 365) {
|
||||
$level = 'warn';
|
||||
$abandoned++;
|
||||
$detail_parts[] = 'no release in >12 months';
|
||||
}
|
||||
}
|
||||
if (!empty($info['active_installs'])) {
|
||||
$detail_parts[] = number_format($info['active_installs']) . ' active installs';
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($update_map[$file])) {
|
||||
$level = 'warn';
|
||||
$detail_parts[] = 'Update available → ' . ($update_map[$file]->new_version ?? '?');
|
||||
$update_avail++;
|
||||
}
|
||||
|
||||
$f[] = $this->finding(
|
||||
'plugin_' . sanitize_key($file),
|
||||
$level,
|
||||
$meta['Name'] ?? $file,
|
||||
(string) ($meta['Version'] ?? ''),
|
||||
implode(' · ', $detail_parts)
|
||||
);
|
||||
}
|
||||
|
||||
// Summary at the top
|
||||
array_unshift($f, $this->finding(
|
||||
'summary', 'info',
|
||||
'Summary',
|
||||
count($plugins) . ' plugins',
|
||||
$update_avail . ' with updates · ' . $abandoned . ' not updated in 12 mo · ' . $removed . ' not in WP.org'
|
||||
));
|
||||
|
||||
return $f;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns ['name' => ..., 'last_updated' => ..., 'active_installs' => ...]
|
||||
* or 'not_in_repo' / 'removed' on failure. Cached 24h per slug via transient.
|
||||
*/
|
||||
private function wp_org_info(string $slug) {
|
||||
$cache_key = 'wph_pi_' . md5($slug);
|
||||
$cached = get_transient($cache_key);
|
||||
if ($cached !== false) return $cached;
|
||||
|
||||
$url = 'https://api.wordpress.org/plugins/info/1.0/' . rawurlencode($slug) . '.json';
|
||||
$resp = wp_remote_get($url, ['timeout' => 4]);
|
||||
if (is_wp_error($resp)) {
|
||||
set_transient($cache_key, 'not_in_repo', HOUR_IN_SECONDS);
|
||||
return 'not_in_repo';
|
||||
}
|
||||
$code = wp_remote_retrieve_response_code($resp);
|
||||
$body = wp_remote_retrieve_body($resp);
|
||||
if ($code === 404) {
|
||||
set_transient($cache_key, 'not_in_repo', DAY_IN_SECONDS);
|
||||
return 'not_in_repo';
|
||||
}
|
||||
$data = json_decode((string) $body, true);
|
||||
if (!is_array($data)) {
|
||||
set_transient($cache_key, 'not_in_repo', HOUR_IN_SECONDS);
|
||||
return 'not_in_repo';
|
||||
}
|
||||
if (isset($data['error'])) {
|
||||
set_transient($cache_key, 'removed', DAY_IN_SECONDS);
|
||||
return 'removed';
|
||||
}
|
||||
$out = [
|
||||
'name' => $data['name'] ?? $slug,
|
||||
'last_updated' => $data['last_updated'] ?? null,
|
||||
'active_installs' => $data['active_installs'] ?? null,
|
||||
];
|
||||
set_transient($cache_key, $out, DAY_IN_SECONDS);
|
||||
return $out;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -14,4 +14,111 @@ return new class extends WPH_Step {
|
||||
'Check that xmlrpc.php is disabled or restricted if not in use',
|
||||
];
|
||||
}
|
||||
|
||||
public function autocheck(array $session_state): array {
|
||||
$f = [];
|
||||
|
||||
// SSL cert expiry — only meaningful for https sites
|
||||
$host = parse_url(get_site_url(), PHP_URL_HOST);
|
||||
$scheme = parse_url(get_site_url(), PHP_URL_SCHEME);
|
||||
if ($scheme === 'https' && $host) {
|
||||
$cert = $this->fetch_cert($host);
|
||||
if (is_array($cert) && !empty($cert['validTo_time_t'])) {
|
||||
$expires = (int) $cert['validTo_time_t'];
|
||||
$days = floor(($expires - time()) / DAY_IN_SECONDS);
|
||||
$level = $days < 0 ? 'bad' : ($days < 30 ? 'warn' : 'ok');
|
||||
$f[] = $this->finding(
|
||||
'ssl_expiry',
|
||||
$level,
|
||||
'SSL certificate',
|
||||
date('Y-m-d', $expires),
|
||||
$days < 0 ? abs($days) . ' day(s) EXPIRED' : 'expires in ' . $days . ' day(s)' . (isset($cert['issuer']['O']) ? ' · issuer: ' . $cert['issuer']['O'] : '')
|
||||
);
|
||||
} else {
|
||||
$f[] = $this->finding('ssl_expiry', 'warn', 'SSL certificate', 'could not fetch', 'TLS handshake to ' . $host . ':443 failed; check manually.');
|
||||
}
|
||||
} else {
|
||||
$f[] = $this->finding('ssl_expiry', 'bad', 'SSL', 'not HTTPS', 'Site URL is not https — install/configure a certificate.');
|
||||
}
|
||||
|
||||
// Administrator audit
|
||||
$admins = get_users(['role' => 'administrator', 'number' => 50]);
|
||||
$f[] = $this->finding(
|
||||
'admin_count',
|
||||
count($admins) > 5 ? 'warn' : 'ok',
|
||||
'Administrator accounts',
|
||||
(string) count($admins),
|
||||
count($admins) > 5 ? 'More than 5 administrators — review whether all are necessary.' : ''
|
||||
);
|
||||
foreach ($admins as $u) {
|
||||
$last_login = get_user_meta($u->ID, 'last_login', true); // if a plugin tracks it
|
||||
$f[] = $this->finding(
|
||||
'admin_' . $u->ID,
|
||||
'info',
|
||||
'admin: ' . $u->user_login,
|
||||
$u->user_email,
|
||||
'Registered ' . $u->user_registered . ($last_login ? ' · last login ' . $last_login : '')
|
||||
);
|
||||
}
|
||||
|
||||
// xmlrpc.php reachability
|
||||
$xmlrpc_url = trailingslashit(get_site_url()) . 'xmlrpc.php';
|
||||
$resp = wp_remote_post($xmlrpc_url, [
|
||||
'timeout' => 5,
|
||||
'headers' => ['Content-Type' => 'text/xml'],
|
||||
'body' => '<?xml version="1.0"?><methodCall><methodName>system.listMethods</methodName></methodCall>',
|
||||
]);
|
||||
if (is_wp_error($resp)) {
|
||||
$f[] = $this->finding('xmlrpc', 'ok', 'xmlrpc.php', 'unreachable', $resp->get_error_message());
|
||||
} else {
|
||||
$code = wp_remote_retrieve_response_code($resp);
|
||||
$body = (string) wp_remote_retrieve_body($resp);
|
||||
$is_open = ($code === 200 && (strpos($body, '<methodResponse>') !== false));
|
||||
$f[] = $this->finding(
|
||||
'xmlrpc',
|
||||
$is_open ? 'warn' : 'ok',
|
||||
'xmlrpc.php',
|
||||
$is_open ? 'open (responds to system.listMethods)' : 'restricted/disabled (' . $code . ')',
|
||||
$is_open ? 'If not in use, consider disabling — common brute-force/DDoS target.' : ''
|
||||
);
|
||||
}
|
||||
|
||||
// Login URL — detect known "hide login" plugins
|
||||
$hide_login_plugins = [
|
||||
'wps-hide-login/wps-hide-login.php' => 'WPS Hide Login',
|
||||
'rename-wp-login/rename-wp-login.php' => 'Rename wp-login.php',
|
||||
];
|
||||
$hidden = false;
|
||||
foreach ($hide_login_plugins as $file => $label) {
|
||||
if (is_plugin_active($file)) { $hidden = $label; break; }
|
||||
}
|
||||
$f[] = $this->finding(
|
||||
'login_url',
|
||||
$hidden ? 'ok' : 'info',
|
||||
'Login URL hardening',
|
||||
$hidden ? 'custom (' . $hidden . ')' : 'default (/wp-admin, /wp-login.php)',
|
||||
$hidden ? '' : 'Not necessarily a problem — confirm with client whether hardening was previously applied.'
|
||||
);
|
||||
|
||||
return $f;
|
||||
}
|
||||
|
||||
private function fetch_cert(string $host) {
|
||||
$ctx = stream_context_create([
|
||||
'ssl' => [
|
||||
'capture_peer_cert' => true,
|
||||
'verify_peer' => false,
|
||||
'verify_peer_name' => false,
|
||||
'SNI_enabled' => true,
|
||||
'peer_name' => $host,
|
||||
],
|
||||
]);
|
||||
$errno = 0; $errstr = '';
|
||||
$sock = @stream_socket_client('ssl://' . $host . ':443', $errno, $errstr, 5, STREAM_CLIENT_CONNECT, $ctx);
|
||||
if (!$sock) return null;
|
||||
$params = stream_context_get_params($sock);
|
||||
fclose($sock);
|
||||
$cert = $params['options']['ssl']['peer_certificate'] ?? null;
|
||||
return $cert ? openssl_x509_parse($cert) : null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -11,4 +11,66 @@ return new class extends WPH_Step {
|
||||
'Check post revisions — if excessive (thousands), note for client discussion on whether a revision limit should be set',
|
||||
];
|
||||
}
|
||||
|
||||
public function autocheck(array $session_state): array {
|
||||
/** @var wpdb $wpdb */
|
||||
global $wpdb;
|
||||
$f = [];
|
||||
|
||||
// Spam comments
|
||||
$counts = wp_count_comments();
|
||||
$f[] = $this->finding(
|
||||
'spam_comments',
|
||||
($counts->spam ?? 0) > 100 ? 'warn' : 'ok',
|
||||
'Spam comments',
|
||||
number_format((int) ($counts->spam ?? 0)),
|
||||
($counts->trash ?? 0) ? number_format((int) $counts->trash) . ' in trash too.' : ''
|
||||
);
|
||||
|
||||
// Post revisions
|
||||
$revisions = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = 'revision'");
|
||||
$level = $revisions > 5000 ? 'warn' : 'ok';
|
||||
$f[] = $this->finding(
|
||||
'revisions',
|
||||
$level,
|
||||
'Post revisions',
|
||||
number_format($revisions),
|
||||
$level === 'warn' ? 'Consider setting WP_POST_REVISIONS to a sane limit (e.g. 10).' : ''
|
||||
);
|
||||
|
||||
// Autoload option size. WP 6.6+ uses 'on'/'auto'/'auto-on' alongside legacy 'yes'.
|
||||
$autoload_in = "autoload IN ('yes', 'on', 'auto', 'auto-on')";
|
||||
$autoload_bytes = (int) $wpdb->get_var("SELECT SUM(LENGTH(option_value)) FROM {$wpdb->options} WHERE $autoload_in");
|
||||
$autoload_count = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->options} WHERE $autoload_in");
|
||||
$autoload_level = $autoload_bytes > 5 * MB_IN_BYTES ? 'warn' : 'ok';
|
||||
$f[] = $this->finding(
|
||||
'autoload',
|
||||
$autoload_level,
|
||||
'Autoload options',
|
||||
$autoload_count . ' rows · ' . size_format($autoload_bytes),
|
||||
$autoload_level === 'warn' ? 'Large autoload payload slows every request — investigate which plugins are responsible.' : ''
|
||||
);
|
||||
|
||||
// Top 3 largest tables. `rows` is reserved in MySQL 8 — alias as table_rows.
|
||||
$tables = $wpdb->get_results("
|
||||
SELECT TABLE_NAME AS name,
|
||||
DATA_LENGTH + INDEX_LENGTH AS bytes,
|
||||
TABLE_ROWS AS table_rows
|
||||
FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
ORDER BY bytes DESC
|
||||
LIMIT 3
|
||||
");
|
||||
foreach ($tables as $i => $t) {
|
||||
$f[] = $this->finding(
|
||||
'big_table_' . $i,
|
||||
'info',
|
||||
($i === 0 ? 'Largest tables — ' : '') . $t->name,
|
||||
size_format((int) $t->bytes),
|
||||
number_format((int) $t->table_rows) . ' rows'
|
||||
);
|
||||
}
|
||||
|
||||
return $f;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user