Some managed hosts (WPE and various resellers, plus some clients' own ops teams) don't let us edit wp-config.php. Add a settings-screen alternative so the plugin can be configured without touching filesystem constants. Storage + resolution: - Two new options: att_hc_api_url and att_hc_api_key, autoload=false on the key so it isn't loaded on every request. - ATT_HC_Api::url() and ATT_HC_Api::key() are the single source of truth now — they return the wp-config constant when defined+non-empty, else the option, else ''. Everything else (request(), config_error(), is_configured(), the admin config-error notice) uses these accessors. - url_from_constant() / key_from_constant() drive per-field locking on the settings page and are also checked by the save handler so a constant-locked field can't be overridden by a crafted POST. UI: - New "Central history server" card at the top of Tools → Site Healthcheck → Settings with URL (type=url) and API key (type=password) inputs. When a constant is defined the field is disabled with a "Set via <constant> constant" hint. - Separate form action/nonce (att_hc_save_api_settings) so it doesn't tangle with the existing Gitea recovery save. - The blocking config-error notice on the main page now offers an "Open settings" button alongside the wp-config.php snippet. Verified with an 18-assertion test suite covering no-config, options- only, http-blocked-with-clear-message, loopback-http-allowed, and constant-wins-over-option. Both PHP 8.3 and PHP 7.4 parse cleanly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
975 lines
50 KiB
PHP
975 lines
50 KiB
PHP
<?php
|
|
if (!defined('ABSPATH')) exit;
|
|
|
|
add_action('admin_menu', 'att_hc_register_menu');
|
|
add_action('admin_post_att_hc_start', 'att_hc_handle_start');
|
|
add_action('admin_post_att_hc_resume', 'att_hc_handle_resume');
|
|
add_action('wp_ajax_att_hc_step_history', 'att_hc_handle_step_history_ajax');
|
|
add_action('admin_post_att_hc_save_step', 'att_hc_handle_save_step');
|
|
add_action('admin_post_att_hc_finish', 'att_hc_handle_finish');
|
|
add_action('admin_post_att_hc_discard', 'att_hc_handle_discard');
|
|
add_action('admin_post_att_hc_download_report', 'att_hc_handle_download_report');
|
|
add_action('admin_post_att_hc_refresh_checks', 'att_hc_handle_refresh_checks');
|
|
add_action('admin_post_att_hc_download_html', 'att_hc_handle_download_html');
|
|
add_action('admin_post_att_hc_email_report', 'att_hc_handle_email_report');
|
|
add_action('admin_post_att_hc_step_action', 'att_hc_handle_step_action');
|
|
add_action('admin_post_att_hc_recovery_install', 'att_hc_handle_recovery_install');
|
|
add_action('admin_post_att_hc_save_settings', 'att_hc_handle_save_settings');
|
|
add_action('admin_post_att_hc_save_api_settings', 'att_hc_handle_save_api_settings');
|
|
add_action('admin_enqueue_scripts', 'att_hc_enqueue_assets');
|
|
|
|
function att_hc_register_menu(): void {
|
|
add_management_page(
|
|
'Site Healthcheck',
|
|
'Site Healthcheck',
|
|
'manage_options',
|
|
'att-site-healthcheck',
|
|
'att_hc_render_admin_page'
|
|
);
|
|
add_submenu_page(
|
|
null, // hidden — reachable via direct URL
|
|
'Site Healthcheck Settings',
|
|
'Site Healthcheck Settings',
|
|
'manage_options',
|
|
'att-site-healthcheck-settings',
|
|
'att_hc_render_settings_page'
|
|
);
|
|
}
|
|
|
|
function att_hc_enqueue_assets($hook): void {
|
|
if ($hook !== 'tools_page_att-site-healthcheck') return;
|
|
wp_register_style('att-hc-admin', false);
|
|
wp_enqueue_style('att-hc-admin');
|
|
wp_add_inline_style('att-hc-admin', att_hc_inline_css());
|
|
}
|
|
|
|
function att_hc_inline_css(): string {
|
|
return '
|
|
.att-hc-card { background:#fff; border:1px solid #c3c4c7; border-radius:6px; padding:1rem 1.25rem; margin-bottom:1rem; }
|
|
.att-hc-card h2 { margin-top:0; }
|
|
.att-hc-step-status { display:inline-block; padding:.1rem .55rem; border-radius:10px; font-size:11px; font-weight:600; text-transform:uppercase; letter-spacing:.04em; }
|
|
.att-hc-status-not_started { background:#f0f0f1; color:#646970; }
|
|
.att-hc-status-done { background:#def7e3; color:#155724; }
|
|
.att-hc-status-skipped { background:#fff3cd; color:#856404; }
|
|
.att-hc-status-blocked { background:#fbeae8; color:#721c24; }
|
|
.att-hc-status-n_a { background:#e2e3e5; color:#41464b; }
|
|
.att-hc-step { padding:1rem 1.25rem; border:1px solid #dcdcde; border-radius:6px; margin-bottom:.75rem; background:#fff; }
|
|
.att-hc-step header { display:flex; justify-content:space-between; align-items:center; gap:1rem; margin-bottom:.5rem; }
|
|
.att-hc-step header h2 { margin:0; font-size:1.1rem; }
|
|
.att-hc-sub-items { margin:.5rem 0 .75rem 1.25rem; padding:0; }
|
|
.att-hc-sub-items li { margin:.15rem 0; }
|
|
.att-hc-watch-outs { background:#fff8e1; border-left:3px solid #f5b800; padding:.4rem .8rem; margin:.5rem 0; font-size:.92em; }
|
|
.att-hc-watch-outs strong { display:block; margin-bottom:.2rem; }
|
|
.att-hc-escalation { background:#fbeae8; border-left:3px solid #c0392b; padding:.4rem .8rem; margin:.5rem 0; font-weight:500; }
|
|
.att-hc-step textarea { width:100%; min-height:60px; }
|
|
.att-hc-progress { font-weight:600; }
|
|
.att-hc-ok { color:#155724; }
|
|
.att-hc-warn { color:#856404; }
|
|
.att-hc-bad { color:#721c24; }
|
|
.att-hc-bootstrap-panel { padding:.6rem 1rem; background:#f6f7f7; border:1px solid #dcdcde; border-radius:6px; margin-bottom:.75rem; }
|
|
.att-hc-bootstrap-panel h3 { margin:0 0 .35rem; font-size:1rem; }
|
|
.att-hc-actions { display:flex; gap:.5rem; align-items:center; margin-top:.4rem; }
|
|
.att-hc-autocheck { background:#f6f7f7; border:1px solid #dcdcde; border-radius:6px; padding:.6rem 1rem; margin:.6rem 0; }
|
|
.att-hc-autocheck header { display:flex; justify-content:space-between; align-items:center; margin:0 0 .4rem; }
|
|
.att-hc-autocheck header h3 { margin:0; font-size:.95rem; }
|
|
.att-hc-autocheck table { width:100%; border-collapse:collapse; }
|
|
.att-hc-autocheck td { padding:.25rem .4rem; vertical-align:top; border-bottom:1px solid #f0f0f1; }
|
|
.att-hc-autocheck tr:last-child td { border-bottom:0; }
|
|
.att-hc-autocheck .lvl { width:1.4rem; text-align:center; font-weight:600; }
|
|
.att-hc-autocheck .lvl-ok { color:#1a8917; }
|
|
.att-hc-autocheck .lvl-warn { color:#b07a00; }
|
|
.att-hc-autocheck .lvl-bad { color:#c0392b; }
|
|
.att-hc-autocheck .lvl-info { color:#646970; }
|
|
.att-hc-autocheck .label { font-weight:600; }
|
|
.att-hc-autocheck .value { color:#1d1d1f; }
|
|
.att-hc-autocheck .detail { color:#646970; font-size:.9em; }
|
|
.att-hc-checked-at { color:#646970; font-size:.85em; }
|
|
.att-hc-layout { display:grid; grid-template-columns: 220px 1fr; gap:1rem; }
|
|
.att-hc-sidebar { position:sticky; top:36px; align-self:start; max-height: calc(100vh - 60px); overflow:auto; }
|
|
.att-hc-sidebar .att-hc-card { padding:.75rem 1rem; }
|
|
.att-hc-sidebar h3 { margin:0 0 .4rem; font-size:.9rem; text-transform:uppercase; letter-spacing:.04em; color:#646970; }
|
|
.att-hc-sidebar ol { margin:0; padding:0; list-style:none; }
|
|
.att-hc-sidebar li { padding:.18rem 0; }
|
|
.att-hc-sidebar a { text-decoration:none; }
|
|
.att-hc-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; }
|
|
.att-hc-sidebar .dot-done { background:#16a34a; box-shadow:0 0 0 1px rgba(0,0,0,.15) inset; }
|
|
.att-hc-sidebar .dot-skipped { background:#f59e0b; }
|
|
.att-hc-sidebar .dot-blocked { background:#dc2626; }
|
|
.att-hc-sidebar .dot-n_a { background:#9ca3af; }
|
|
.att-hc-sidebar li:has(.dot-done) a { color:#15803d; }
|
|
.att-hc-diff { background:#eef4fb; border:1px solid #cfe0f3; padding:.6rem 1rem; border-radius:6px; margin:.5rem 0; font-size:.9em; }
|
|
.att-hc-diff strong { display:inline-block; margin-right:.4rem; }
|
|
.att-hc-diff .delta-new { color:#c0392b; }
|
|
.att-hc-diff .delta-resolved { color:#1a8917; }
|
|
.att-hc-diff .delta-changed { color:#b07a00; }
|
|
@media (max-width: 980px) { .att-hc-layout { grid-template-columns: 1fr; } .att-hc-sidebar { position: static; max-height: none; } }
|
|
';
|
|
}
|
|
|
|
function att_hc_render_admin_page(): void {
|
|
if (!current_user_can('manage_options')) wp_die('Forbidden');
|
|
|
|
$session = ATT_HC_Session::current();
|
|
|
|
echo '<div class="wrap">';
|
|
echo '<h1>Site Healthcheck</h1>';
|
|
|
|
if ($cfg_err = ATT_HC_Api::config_error()) {
|
|
$settings_url = admin_url('tools.php?page=att-site-healthcheck-settings');
|
|
echo '<div class="notice notice-error"><p><strong>Central history server not usable:</strong> ' . esc_html($cfg_err) . '</p>';
|
|
echo '<p><a class="button button-primary" href="' . esc_url($settings_url) . '">Open settings</a> to set the server URL and API key, or add the following to <code>wp-config.php</code>:</p>';
|
|
echo '<pre>define(\'ATT_HC_API_URL\', \'https://your-history-server.example.com\');' . "\n" . 'define(\'ATT_HC_API_KEY\', \'<shared secret>\');</pre></div>';
|
|
echo '</div>';
|
|
return;
|
|
}
|
|
|
|
if ($msg = get_transient('att_hc_install_message')) {
|
|
delete_transient('att_hc_install_message');
|
|
echo '<div class="notice notice-success is-dismissible"><p>' . esc_html($msg) . '</p></div>';
|
|
}
|
|
|
|
if ($err = get_transient('att_hc_api_error')) {
|
|
delete_transient('att_hc_api_error');
|
|
echo '<div class="notice notice-error"><p><strong>Central history server:</strong> ' . esc_html($err) . '</p></div>';
|
|
}
|
|
|
|
if (!$session) {
|
|
att_hc_render_start_panel();
|
|
echo '</div>';
|
|
return;
|
|
}
|
|
|
|
if ($session->is_finished()) {
|
|
att_hc_render_finished_panel($session);
|
|
echo '</div>';
|
|
return;
|
|
}
|
|
|
|
att_hc_render_active_session($session);
|
|
echo '</div>';
|
|
}
|
|
|
|
function att_hc_render_start_panel(): void {
|
|
// Site key for the lookup is either user-specified (?site_key=...) or
|
|
// the normalised current site URL. The technician can override on submit.
|
|
$default_key = ATT_HC_Session::normalise_site_url(get_site_url());
|
|
$site_key = isset($_GET['site_key']) && is_string($_GET['site_key']) && $_GET['site_key'] !== ''
|
|
? sanitize_text_field(wp_unslash((string) $_GET['site_key']))
|
|
: $default_key;
|
|
|
|
// Best-effort fetches — surface a notice on failure but still let the
|
|
// tech start a fresh session. (Resume needs a successful list call to
|
|
// know which session to resume.)
|
|
$incomplete = [];
|
|
$recent = [];
|
|
$lookup_err = null;
|
|
try {
|
|
$resp = ATT_HC_Api::list_healthchecks($site_key, false, 20);
|
|
foreach ($resp['healthchecks'] ?? [] as $hc) {
|
|
if (empty($hc['finished_at'])) $incomplete[] = $hc;
|
|
}
|
|
} catch (ATT_HC_Api_Exception $e) {
|
|
$lookup_err = $e->getMessage();
|
|
}
|
|
try {
|
|
$sites = ATT_HC_Api::recent_sites(20);
|
|
$recent = $sites['sites'] ?? [];
|
|
} catch (ATT_HC_Api_Exception $e) {
|
|
// No-op — the recent dropdown is just a convenience.
|
|
}
|
|
?>
|
|
<div class="att-hc-card">
|
|
<h2>Start a healthcheck</h2>
|
|
<?php if ($lookup_err): ?>
|
|
<div class="notice notice-warning inline"><p>Could not look up history for this engagement: <?php echo esc_html($lookup_err); ?>. You can still start a new session.</p></div>
|
|
<?php endif; ?>
|
|
|
|
<form method="get" action="<?php echo esc_url(admin_url('tools.php')); ?>" style="margin-bottom:1rem">
|
|
<input type="hidden" name="page" value="att-site-healthcheck">
|
|
<p>
|
|
<label for="att-hc-site-key"><strong>Site / engagement key</strong></label><br>
|
|
<input id="att-hc-site-key" type="text" name="site_key" value="<?php echo esc_attr($site_key); ?>" list="att-hc-recent-sites" class="regular-text" required>
|
|
<button class="button" type="submit">Look up engagement</button>
|
|
<?php if ($site_key !== $default_key): ?>
|
|
<a class="button button-link" href="<?php echo esc_url(admin_url('tools.php?page=att-site-healthcheck')); ?>">Reset to this site</a>
|
|
<?php endif; ?>
|
|
</p>
|
|
<?php if ($recent): ?>
|
|
<datalist id="att-hc-recent-sites">
|
|
<?php foreach ($recent as $r): ?>
|
|
<option value="<?php echo esc_attr($r['site_key']); ?>">
|
|
<?php endforeach; ?>
|
|
</datalist>
|
|
<?php endif; ?>
|
|
<p class="description">
|
|
Used to group runs that span dev + live for the same engagement. Defaults to the host of this site.
|
|
<?php if ($recent): ?>Type to autocomplete from <?php echo count($recent); ?> recent engagement(s).<?php endif; ?>
|
|
</p>
|
|
</form>
|
|
|
|
<?php if ($incomplete): ?>
|
|
<h3>In-progress healthchecks for <code><?php echo esc_html($site_key); ?></code></h3>
|
|
<p>Pick up where a previous session left off, or start a fresh one below.</p>
|
|
<table class="widefat striped" style="margin-bottom:1rem">
|
|
<thead><tr><th>Started</th><th>Last activity</th><th>Reporting URL</th><th>Action</th></tr></thead>
|
|
<tbody>
|
|
<?php foreach ($incomplete as $hc): ?>
|
|
<tr>
|
|
<td><?php echo esc_html(date('Y-m-d H:i', (int) $hc['started_at'])); ?></td>
|
|
<td><?php echo esc_html(human_time_diff((int) $hc['updated_at'], time())); ?> ago</td>
|
|
<td><code><?php echo esc_html((string) $hc['reporting_url']); ?></code></td>
|
|
<td>
|
|
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" style="display:inline">
|
|
<?php wp_nonce_field('att_hc_resume_' . $hc['id']); ?>
|
|
<input type="hidden" name="action" value="att_hc_resume">
|
|
<input type="hidden" name="id" value="<?php echo esc_attr($hc['id']); ?>">
|
|
<button class="button">Resume</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
<?php endif; ?>
|
|
|
|
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
|
|
<?php wp_nonce_field('att_hc_start'); ?>
|
|
<input type="hidden" name="action" value="att_hc_start">
|
|
<input type="hidden" name="site_key" value="<?php echo esc_attr($site_key); ?>">
|
|
<button class="button button-primary"><?php echo $incomplete ? 'Start a fresh healthcheck for this engagement' : 'Start new healthcheck'; ?></button>
|
|
</form>
|
|
</div>
|
|
<?php
|
|
ATT_HC_Recovery_Bootstrap::render_status();
|
|
}
|
|
|
|
function att_hc_render_active_session(ATT_HC_Session $session): void {
|
|
$progress = $session->progress();
|
|
$tech = get_userdata($session->technician_id());
|
|
?>
|
|
<div class="att-hc-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="att-hc-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="att-hc-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('att_hc_finish'); ?>
|
|
<input type="hidden" name="action" value="att_hc_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('att_hc_discard'); ?>
|
|
<input type="hidden" name="action" value="att_hc_discard">
|
|
<button class="button button-link-delete">Discard</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<?php
|
|
|
|
ATT_HC_Recovery_Bootstrap::render_status();
|
|
att_hc_render_blocked_summary($session);
|
|
att_hc_render_diff_summary($session);
|
|
|
|
// One round-trip up-front so each step card knows how many prior notes
|
|
// exist (the "(N)" badge). Detail rows are lazy-loaded on expand.
|
|
$step_counts = [];
|
|
try {
|
|
$resp = ATT_HC_Api::step_counts($session->site_key(), $session->id());
|
|
$step_counts = $resp['counts'] ?? [];
|
|
} catch (ATT_HC_Api_Exception $e) {
|
|
// History badges are a nice-to-have — don't block the page.
|
|
}
|
|
|
|
att_hc_print_step_history_assets($session);
|
|
|
|
echo '<div class="att-hc-layout">';
|
|
att_hc_render_sidebar($session);
|
|
echo '<div>';
|
|
foreach (ATT_HC_Steps::instance()->all() as $step) {
|
|
att_hc_render_step_card($session, $step, (int) ($step_counts[$step->id()] ?? 0));
|
|
}
|
|
echo '</div></div>';
|
|
}
|
|
|
|
function att_hc_print_step_history_assets(ATT_HC_Session $session): void {
|
|
$cfg = [
|
|
'ajaxUrl' => admin_url('admin-ajax.php'),
|
|
'nonce' => wp_create_nonce('att_hc_step_history'),
|
|
'reportingUrl' => $session->reporting_url(),
|
|
];
|
|
?>
|
|
<style>
|
|
.att-hc-history { margin: .6rem 0; }
|
|
.att-hc-history > summary { cursor: pointer; color: #2271b1; font-size: .9em; padding: .15rem 0; }
|
|
.att-hc-history > summary:hover { color: #135e96; }
|
|
.att-hc-history[open] > summary { font-weight: 600; }
|
|
.att-hc-history-body { margin-top: .5rem; padding: .5rem .75rem; background: #f6f7f7; border: 1px solid #dcdcde; border-radius: 4px; font-size: .92em; }
|
|
.att-hc-history-entry { padding: .4rem 0; border-bottom: 1px dashed #dcdcde; }
|
|
.att-hc-history-entry:last-child { border-bottom: 0; }
|
|
.att-hc-history-meta { color: #646970; font-size: .9em; margin-bottom: .15rem; }
|
|
.att-hc-history-notes { white-space: pre-wrap; margin: .2rem 0 0; }
|
|
.att-hc-history-notes.empty { color: #8c8f94; font-style: italic; }
|
|
.att-hc-history-reporting { color: #646970; font-size: .85em; margin-top: .15rem; }
|
|
.att-hc-history-err { color: #b32d2e; }
|
|
</style>
|
|
<script>
|
|
(function () {
|
|
var cfg = <?php echo wp_json_encode($cfg); ?>;
|
|
function statusLabel(s) { return s.replace(/_/g, ' '); }
|
|
function escapeHtml(s) {
|
|
return String(s).replace(/[&<>"']/g, function (c) {
|
|
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
|
|
});
|
|
}
|
|
function fmtDate(unix) {
|
|
var d = new Date(unix * 1000);
|
|
return d.toISOString().slice(0, 10) + ' ' + d.toTimeString().slice(0, 5);
|
|
}
|
|
function renderRows(rows) {
|
|
if (!rows.length) return '<p>No prior notes for this step.</p>';
|
|
return rows.map(function (r) {
|
|
var notes = (r.notes || '').trim();
|
|
var notesHtml = notes
|
|
? '<p class="att-hc-history-notes">' + escapeHtml(notes) + '</p>'
|
|
: '<p class="att-hc-history-notes empty">(no notes)</p>';
|
|
var diffUrl = r.step_reporting_url && r.step_reporting_url !== cfg.reportingUrl
|
|
? '<p class="att-hc-history-reporting">Reported from <code>' + escapeHtml(r.step_reporting_url) + '</code></p>'
|
|
: '';
|
|
return '<div class="att-hc-history-entry">' +
|
|
'<div class="att-hc-history-meta">' +
|
|
fmtDate(r.started_at) +
|
|
' · <span class="att-hc-step-status att-hc-status-' + escapeHtml(r.status) + '">' + escapeHtml(statusLabel(r.status)) + '</span>' +
|
|
(r.finished_at ? '' : ' · <em>still in progress</em>') +
|
|
'</div>' +
|
|
notesHtml +
|
|
diffUrl +
|
|
'</div>';
|
|
}).join('');
|
|
}
|
|
document.querySelectorAll('details.att-hc-history').forEach(function (det) {
|
|
det.addEventListener('toggle', function () {
|
|
if (!det.open || det.dataset.loaded === 'yes') return;
|
|
det.dataset.loaded = 'yes';
|
|
var body = det.querySelector('.att-hc-history-body');
|
|
var stepId = det.dataset.stepId;
|
|
body.textContent = 'Loading…';
|
|
var form = new FormData();
|
|
form.append('action', 'att_hc_step_history');
|
|
form.append('step_id', stepId);
|
|
form.append('nonce', cfg.nonce);
|
|
fetch(cfg.ajaxUrl, { method: 'POST', credentials: 'same-origin', body: form })
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (j) {
|
|
if (!j || !j.success) {
|
|
body.innerHTML = '<p class="att-hc-history-err">Could not load: ' + escapeHtml((j && j.data) || 'unknown error') + '</p>';
|
|
det.dataset.loaded = 'no'; // allow retry on next toggle
|
|
return;
|
|
}
|
|
body.innerHTML = renderRows(j.data.history || []);
|
|
})
|
|
.catch(function (e) {
|
|
body.innerHTML = '<p class="att-hc-history-err">Network error: ' + escapeHtml(e.message) + '</p>';
|
|
det.dataset.loaded = 'no';
|
|
});
|
|
});
|
|
});
|
|
})();
|
|
</script>
|
|
<?php
|
|
}
|
|
|
|
function att_hc_render_sidebar(ATT_HC_Session $session): void {
|
|
echo '<aside class="att-hc-sidebar"><div class="att-hc-card"><h3>Steps</h3><ol>';
|
|
foreach (ATT_HC_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 att_hc_render_blocked_summary(ATT_HC_Session $session): void {
|
|
$blocked = [];
|
|
foreach (ATT_HC_Steps::instance()->all() as $step) {
|
|
$state = $session->step_state($step->id());
|
|
if ($state['status'] === ATT_HC_Session::STATUS_BLOCKED) {
|
|
$blocked[] = ['step' => $step, 'state' => $state];
|
|
}
|
|
}
|
|
if (!$blocked) return;
|
|
echo '<div class="att-hc-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 att_hc_render_diff_summary(ATT_HC_Session $session): void {
|
|
$prev = ATT_HC_Session::previous();
|
|
if (!$prev) return;
|
|
|
|
// Aggregate findings by step+id from each session
|
|
$current_idx = [];
|
|
foreach (ATT_HC_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 (ATT_HC_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="att-hc-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 att_hc_render_step_card(ATT_HC_Session $session, ATT_HC_Step $step, int $history_count = 0): void {
|
|
$state = $session->step_state($step->id());
|
|
$status = $state['status'];
|
|
$notes = $state['notes'];
|
|
?>
|
|
<div class="att-hc-step" id="step-<?php echo esc_attr($step->id()); ?>">
|
|
<header>
|
|
<h2><?php echo esc_html($step->title()); ?></h2>
|
|
<span class="att-hc-step-status att-hc-status-<?php echo esc_attr($status); ?>"><?php echo esc_html(str_replace('_', ' ', $status)); ?></span>
|
|
</header>
|
|
<?php if ($history_count > 0): ?>
|
|
<details class="att-hc-history" data-step-id="<?php echo esc_attr($step->id()); ?>" data-loaded="no">
|
|
<summary>Previous notes for this step (<?php echo (int) $history_count; ?>)</summary>
|
|
<div class="att-hc-history-body"></div>
|
|
</details>
|
|
<?php endif; ?>
|
|
<?php if ($blurb = $step->blurb()): ?>
|
|
<p><?php echo esc_html($blurb); ?></p>
|
|
<?php endif; ?>
|
|
<?php if ($items = $step->sub_items()): ?>
|
|
<ul class="att-hc-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="att-hc-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 === ATT_HC_Session::STATUS_BLOCKED && ($esc = $step->escalation())): ?>
|
|
<div class="att-hc-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.
|
|
att_hc_render_autocheck($session, $step);
|
|
$step->render_extra($session->data());
|
|
?>
|
|
|
|
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" class="att-hc-step-save">
|
|
<?php wp_nonce_field('att_hc_save_step_' . $step->id()); ?>
|
|
<input type="hidden" name="action" value="att_hc_save_step">
|
|
<input type="hidden" name="step" value="<?php echo esc_attr($step->id()); ?>">
|
|
<p>
|
|
<label>
|
|
<strong>Status:</strong>
|
|
<select name="status">
|
|
<?php foreach (ATT_HC_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 att_hc_render_finished_panel(ATT_HC_Session $session): void {
|
|
$report = att_hc_build_markdown_report($session);
|
|
$admin_email = get_option('admin_email');
|
|
?>
|
|
<div class="att-hc-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="att-hc-actions">
|
|
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" style="display:inline">
|
|
<?php wp_nonce_field('att_hc_download_report'); ?>
|
|
<input type="hidden" name="action" value="att_hc_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('att_hc_download_html'); ?>
|
|
<input type="hidden" name="action" value="att_hc_download_html">
|
|
<button class="button">Download HTML</button>
|
|
</form>
|
|
<button class="button" onclick="navigator.clipboard.writeText(document.getElementById('att-hc-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('att_hc_email_report'); ?>
|
|
<input type="hidden" name="action" value="att_hc_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('att_hc_discard'); ?>
|
|
<input type="hidden" name="action" value="att_hc_discard">
|
|
<button class="button button-link-delete">Discard</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<div class="att-hc-card">
|
|
<h2>Report preview</h2>
|
|
<pre id="att-hc-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 att_hc_handle_start(): void {
|
|
if (!current_user_can('manage_options')) wp_die('Forbidden');
|
|
check_admin_referer('att_hc_start');
|
|
// Double-submit / double-click guard: if an active session already exists,
|
|
// ignore the second start and just send the user to it. Otherwise we'd POST
|
|
// again, hit the (site_key, started_at) unique constraint, and surface a
|
|
// confusing "duplicate" error.
|
|
if (ATT_HC_Session::current()) {
|
|
wp_safe_redirect(admin_url('tools.php?page=att-site-healthcheck'));
|
|
exit;
|
|
}
|
|
$site_key = isset($_POST['site_key']) ? sanitize_text_field(wp_unslash((string) $_POST['site_key'])) : null;
|
|
try {
|
|
ATT_HC_Session::start(get_current_user_id(), $site_key);
|
|
} catch (ATT_HC_Api_Exception $e) {
|
|
set_transient('att_hc_api_error', 'Could not register the session with the central server: ' . $e->getMessage(), 60);
|
|
}
|
|
wp_safe_redirect(admin_url('tools.php?page=att-site-healthcheck'));
|
|
exit;
|
|
}
|
|
|
|
function att_hc_handle_step_history_ajax(): void {
|
|
if (!current_user_can('manage_options')) wp_send_json_error('Forbidden', 403);
|
|
if (!check_ajax_referer('att_hc_step_history', 'nonce', false)) {
|
|
wp_send_json_error('Bad nonce', 403);
|
|
}
|
|
$step_id = isset($_POST['step_id']) ? sanitize_key((string) $_POST['step_id']) : '';
|
|
if ($step_id === '') wp_send_json_error('Missing step_id', 400);
|
|
$session = ATT_HC_Session::current();
|
|
if (!$session) wp_send_json_error('No active session', 400);
|
|
try {
|
|
$resp = ATT_HC_Api::step_history($step_id, $session->site_key(), 5, $session->id());
|
|
wp_send_json_success($resp);
|
|
} catch (ATT_HC_Api_Exception $e) {
|
|
wp_send_json_error($e->getMessage(), 502);
|
|
}
|
|
}
|
|
|
|
function att_hc_handle_resume(): void {
|
|
if (!current_user_can('manage_options')) wp_die('Forbidden');
|
|
$id = isset($_POST['id']) ? sanitize_text_field(wp_unslash((string) $_POST['id'])) : '';
|
|
check_admin_referer('att_hc_resume_' . $id);
|
|
if (ATT_HC_Session::current()) {
|
|
// Same guard as start — don't clobber an active session on a stray click.
|
|
wp_safe_redirect(admin_url('tools.php?page=att-site-healthcheck'));
|
|
exit;
|
|
}
|
|
try {
|
|
ATT_HC_Session::resume($id);
|
|
} catch (ATT_HC_Api_Exception $e) {
|
|
set_transient('att_hc_api_error', 'Could not resume that session: ' . $e->getMessage(), 60);
|
|
}
|
|
wp_safe_redirect(admin_url('tools.php?page=att-site-healthcheck'));
|
|
exit;
|
|
}
|
|
|
|
function att_hc_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('att_hc_save_step_' . $step_id);
|
|
$session = ATT_HC_Session::current();
|
|
if (!$session || $session->is_finished()) wp_die('No active session.');
|
|
if (!ATT_HC_Steps::instance()->get($step_id)) wp_die('Unknown step.');
|
|
$status = isset($_POST['status']) ? sanitize_key((string) $_POST['status']) : ATT_HC_Session::STATUS_NOT_STARTED;
|
|
$notes = isset($_POST['notes']) ? wp_unslash((string) $_POST['notes']) : '';
|
|
try {
|
|
$session->update_step($step_id, $status, $notes);
|
|
} catch (ATT_HC_Api_Exception $e) {
|
|
set_transient('att_hc_api_error', 'Step not saved (central server rejected the write): ' . $e->getMessage(), 60);
|
|
}
|
|
wp_safe_redirect(admin_url('tools.php?page=att-site-healthcheck#step-' . rawurlencode($step_id)));
|
|
exit;
|
|
}
|
|
|
|
function att_hc_handle_finish(): void {
|
|
if (!current_user_can('manage_options')) wp_die('Forbidden');
|
|
check_admin_referer('att_hc_finish');
|
|
$session = ATT_HC_Session::current();
|
|
if (!$session) wp_die('No active session.');
|
|
try {
|
|
$session->finish();
|
|
} catch (ATT_HC_Api_Exception $e) {
|
|
set_transient('att_hc_api_error', 'Could not mark session finished on the central server: ' . $e->getMessage(), 60);
|
|
}
|
|
wp_safe_redirect(admin_url('tools.php?page=att-site-healthcheck'));
|
|
exit;
|
|
}
|
|
|
|
function att_hc_handle_discard(): void {
|
|
if (!current_user_can('manage_options')) wp_die('Forbidden');
|
|
check_admin_referer('att_hc_discard');
|
|
ATT_HC_Session::discard();
|
|
wp_safe_redirect(admin_url('tools.php?page=att-site-healthcheck'));
|
|
exit;
|
|
}
|
|
|
|
function att_hc_render_autocheck(ATT_HC_Session $session, ATT_HC_Step $step): void {
|
|
if (!$step->has_autocheck()) return;
|
|
$result = $session->get_autocheck($step->id());
|
|
?>
|
|
<div class="att-hc-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('att_hc_refresh_checks_' . $step->id()); ?>
|
|
<input type="hidden" name="action" value="att_hc_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="att-hc-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 att_hc_render_settings_page(): void {
|
|
if (!current_user_can('manage_options')) wp_die('Forbidden');
|
|
$c = ATT_HC_Recovery_Installer::config();
|
|
$o = ATT_HC_Recovery_Installer::config_origin();
|
|
$saved = isset($_GET['att_hc_saved']);
|
|
?>
|
|
<div class="wrap">
|
|
<h1>Site Healthcheck — Settings</h1>
|
|
<p><a href="<?php echo esc_url(admin_url('tools.php?page=att-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="att-hc-card">
|
|
<h2>Central history server</h2>
|
|
<p>The plugin writes every step to a central server so healthcheck history survives per-engagement plugin uninstalls and follows the site across dev/live URLs.</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). Using constants is preferred where the host allows it — the API key stored as an option is visible to WP admins and anyone with DB access.</p>
|
|
|
|
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
|
|
<?php wp_nonce_field('att_hc_save_api_settings'); ?>
|
|
<input type="hidden" name="action" value="att_hc_save_api_settings">
|
|
<table class="form-table">
|
|
<tr>
|
|
<th><label for="att-hc-api-url">Server URL</label></th>
|
|
<td>
|
|
<input id="att-hc-api-url" type="url" name="api_url" value="<?php echo esc_attr(ATT_HC_Api::url()); ?>" class="regular-text" placeholder="https://healthcheck-history.example.com" <?php disabled(ATT_HC_Api::url_from_constant()); ?>>
|
|
<?php if (ATT_HC_Api::url_from_constant()): ?>
|
|
<p class="description">Set via <code>ATT_HC_API_URL</code> constant.</p>
|
|
<?php else: ?>
|
|
<p class="description">Must start with <code>https://</code>. No trailing slash necessary.</p>
|
|
<?php endif; ?>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<th><label for="att-hc-api-key">API key</label></th>
|
|
<td>
|
|
<input id="att-hc-api-key" type="password" name="api_key" value="<?php echo esc_attr(ATT_HC_Api::key()); ?>" class="regular-text" autocomplete="new-password" <?php disabled(ATT_HC_Api::key_from_constant()); ?>>
|
|
<?php if (ATT_HC_Api::key_from_constant()): ?>
|
|
<p class="description">Set via <code>ATT_HC_API_KEY</code> constant.</p>
|
|
<?php else: ?>
|
|
<p class="description">Shared secret. Copy it from the server's <code>config.php</code>. Stored in the WP options table.</p>
|
|
<?php endif; ?>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
<p>
|
|
<button class="button button-primary">Save settings</button>
|
|
</p>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="att-hc-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('att_hc_save_settings'); ?>
|
|
<input type="hidden" name="action" value="att_hc_save_settings">
|
|
<table class="form-table">
|
|
<tr>
|
|
<th><label for="att-hc-host">Gitea host</label></th>
|
|
<td>
|
|
<input id="att-hc-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>ATT_HC_GITEA_HOST</code> constant.</p><?php endif; ?>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<th><label for="att-hc-owner">Owner</label></th>
|
|
<td>
|
|
<input id="att-hc-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>ATT_HC_GITEA_OWNER</code> constant.</p><?php endif; ?>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<th><label for="att-hc-repo">Repo</label></th>
|
|
<td>
|
|
<input id="att-hc-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>ATT_HC_GITEA_REPO</code> constant.</p><?php endif; ?>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<th><label for="att-hc-token">Token</label></th>
|
|
<td>
|
|
<input id="att-hc-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>ATT_HC_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 (ATT_HC_Recovery_Installer::is_configured() && !ATT_HC_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('att_hc_recovery_install'); ?>
|
|
<input type="hidden" name="action" value="att_hc_recovery_install">
|
|
<button class="button button-primary">Install from gitea (latest)</button>
|
|
</form>
|
|
<?php elseif (ATT_HC_Recovery_Bootstrap::is_installed()): ?>
|
|
<hr>
|
|
<p class="att-hc-ok">✓ Site Recovery is already installed at <code><?php echo esc_html((string) ATT_HC_Recovery_Bootstrap::plugin_file()); ?></code>.</p>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
<?php
|
|
}
|
|
|
|
function att_hc_handle_save_settings(): void {
|
|
if (!current_user_can('manage_options')) wp_die('Forbidden');
|
|
check_admin_referer('att_hc_save_settings');
|
|
ATT_HC_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=att-site-healthcheck-settings&att_hc_saved=1'));
|
|
exit;
|
|
}
|
|
|
|
function att_hc_handle_save_api_settings(): void {
|
|
if (!current_user_can('manage_options')) wp_die('Forbidden');
|
|
check_admin_referer('att_hc_save_api_settings');
|
|
|
|
// Only touch options the user is actually allowed to write. If a constant
|
|
// is defined for a field, we ignore the submitted value (the input is also
|
|
// disabled in the UI, but belt-and-braces on the handler too).
|
|
if (!ATT_HC_Api::url_from_constant()) {
|
|
$url = trim((string) wp_unslash($_POST['api_url'] ?? ''));
|
|
$url = rtrim($url, '/');
|
|
update_option(ATT_HC_Api::OPT_URL, $url, false);
|
|
}
|
|
if (!ATT_HC_Api::key_from_constant()) {
|
|
$key = trim((string) wp_unslash($_POST['api_key'] ?? ''));
|
|
update_option(ATT_HC_Api::OPT_KEY, $key, false);
|
|
}
|
|
|
|
wp_safe_redirect(admin_url('tools.php?page=att-site-healthcheck-settings&att_hc_saved=1'));
|
|
exit;
|
|
}
|
|
|
|
function att_hc_handle_recovery_install(): void {
|
|
if (!current_user_can('install_plugins') || !current_user_can('activate_plugins')) wp_die('Forbidden');
|
|
check_admin_referer('att_hc_recovery_install');
|
|
@set_time_limit(120);
|
|
$result = ATT_HC_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=att-site-healthcheck-settings')) . '">Back to settings</a></p>');
|
|
}
|
|
set_transient('att_hc_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=att-site-healthcheck'));
|
|
exit;
|
|
}
|
|
|
|
function att_hc_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('att_hc_step_action_' . $step_id . '_' . $action_name);
|
|
$session = ATT_HC_Session::current();
|
|
if (!$session || $session->is_finished()) wp_die('No active session.');
|
|
$step = ATT_HC_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;
|
|
try {
|
|
$session->set_autocheck($step_id, $findings);
|
|
} catch (ATT_HC_Api_Exception $e) {
|
|
set_transient('att_hc_api_error', 'Step action ran but the result could not be saved to the central server: ' . $e->getMessage(), 60);
|
|
}
|
|
}
|
|
|
|
wp_safe_redirect(admin_url('tools.php?page=att-site-healthcheck#step-' . rawurlencode($step_id)));
|
|
exit;
|
|
}
|
|
|
|
function att_hc_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('att_hc_refresh_checks_' . $step_id);
|
|
$session = ATT_HC_Session::current();
|
|
if (!$session || $session->is_finished()) wp_die('No active session.');
|
|
$step = ATT_HC_Steps::instance()->get($step_id);
|
|
if (!$step) wp_die('Unknown step.');
|
|
@set_time_limit(60);
|
|
$findings = $step->autocheck($session->data());
|
|
try {
|
|
$session->set_autocheck($step_id, $findings);
|
|
} catch (ATT_HC_Api_Exception $e) {
|
|
set_transient('att_hc_api_error', 'Autocheck ran but the result could not be saved to the central server: ' . $e->getMessage(), 60);
|
|
}
|
|
wp_safe_redirect(admin_url('tools.php?page=att-site-healthcheck#step-' . rawurlencode($step_id)));
|
|
exit;
|
|
}
|
|
|
|
function att_hc_handle_download_report(): void {
|
|
if (!current_user_can('manage_options')) wp_die('Forbidden');
|
|
check_admin_referer('att_hc_download_report');
|
|
$session = ATT_HC_Session::current();
|
|
if (!$session) wp_die('No session.');
|
|
att_hc_stream_report($session, 'md');
|
|
}
|
|
|
|
function att_hc_handle_download_html(): void {
|
|
if (!current_user_can('manage_options')) wp_die('Forbidden');
|
|
check_admin_referer('att_hc_download_html');
|
|
$session = ATT_HC_Session::current();
|
|
if (!$session) wp_die('No session.');
|
|
att_hc_stream_report($session, 'html');
|
|
}
|
|
|
|
function att_hc_stream_report(ATT_HC_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="att-hc-report-' . $host . '-' . $stamp . '.html"');
|
|
echo att_hc_build_html_report($session);
|
|
} else {
|
|
header('Content-Type: text/markdown; charset=UTF-8');
|
|
header('Content-Disposition: attachment; filename="att-hc-report-' . $host . '-' . $stamp . '.md"');
|
|
echo att_hc_build_markdown_report($session);
|
|
}
|
|
exit;
|
|
}
|
|
|
|
function att_hc_handle_email_report(): void {
|
|
if (!current_user_can('manage_options')) wp_die('Forbidden');
|
|
check_admin_referer('att_hc_email_report');
|
|
$session = ATT_HC_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 = att_hc_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=att-site-healthcheck&att_hc_mail=' . ($ok ? '1' : '0')));
|
|
exit;
|
|
}
|