Recovery plugin: folder-agnostic detection + one-click install from Gitea

Detection (recovery-bootstrap.php):
- Match on plugin Name + Author instead of hard-coded folder slug, so
  the recovery plugin is found regardless of whether it was unpacked
  as site-recovery/, wp-site-recovery/, or anything else.

One-click install (recovery-installer.php — new):
- Pulls site-recovery from a private Gitea repo via the standard archive
  endpoint with token auth.
- Ref resolution order: latest release → latest tag → main branch HEAD.
  Lets us tag pinned releases in Gitea later without changing code.
- upgrader_source_selection filter forces the unpacked folder name to
  'site-recovery' regardless of the gitea wrapper-folder suffix.
- Config from constants in wp-config.php (WPH_GITEA_HOST/OWNER/REPO/TOKEN)
  beats DB option storage. Constants are visually locked in the settings
  UI so admins can see they're inherited.

Settings page (hidden submenu, reachable at Tools → Site Healthcheck
→ 'Configure gitea source'):
- Host / Owner / Repo / Token fields
- Inline 'Install now' button when configured + plugin not installed

Bootstrap status panel:
- Shows the recovery plugin's actual file path when active
- 'Install from gitea (latest)' button when configured + missing
- 'Configure gitea source' button when not configured + missing

Install handler shows a success message via transient on the main
healthcheck page after a successful install + activate.
This commit is contained in:
2026-06-12 09:43:55 +01:00
parent 79cb06e705
commit 8ceb756404
4 changed files with 339 additions and 10 deletions

View File

@@ -11,6 +11,8 @@ add_action('admin_post_wph_refresh_checks', 'wph_handle_refresh_checks');
add_action('admin_post_wph_download_html', 'wph_handle_download_html');
add_action('admin_post_wph_email_report', 'wph_handle_email_report');
add_action('admin_post_wph_step_action', 'wph_handle_step_action');
add_action('admin_post_wph_recovery_install', 'wph_handle_recovery_install');
add_action('admin_post_wph_save_settings', 'wph_handle_save_settings');
add_action('admin_enqueue_scripts', 'wph_enqueue_assets');
function wph_register_menu(): void {
@@ -21,6 +23,14 @@ function wph_register_menu(): void {
'site-healthcheck',
'wph_render_admin_page'
);
add_submenu_page(
null, // hidden — reachable via direct URL
'Site Healthcheck Settings',
'Site Healthcheck Settings',
'manage_options',
'site-healthcheck-settings',
'wph_render_settings_page'
);
}
function wph_enqueue_assets($hook): void {
@@ -100,6 +110,11 @@ function wph_render_admin_page(): void {
echo '<div class="wrap">';
echo '<h1>Site Healthcheck</h1>';
if ($msg = get_transient('wph_install_message')) {
delete_transient('wph_install_message');
echo '<div class="notice notice-success is-dismissible"><p>' . esc_html($msg) . '</p></div>';
}
if (!$session) {
wph_render_start_panel();
echo '</div>';
@@ -429,6 +444,103 @@ function wph_render_autocheck(WPH_Session $session, WPH_Step $step): void {
<?php
}
function wph_render_settings_page(): void {
if (!current_user_can('manage_options')) wp_die('Forbidden');
$c = WPH_Recovery_Installer::config();
$o = WPH_Recovery_Installer::config_origin();
$saved = isset($_GET['wph_saved']);
?>
<div class="wrap">
<h1>Site Healthcheck — Settings</h1>
<p><a href="<?php echo esc_url(admin_url('tools.php?page=site-healthcheck')); ?>">&larr; Back to healthcheck</a></p>
<?php if ($saved): ?><div class="notice notice-success is-dismissible"><p>Saved.</p></div><?php endif; ?>
<div class="wph-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('wph_save_settings'); ?>
<input type="hidden" name="action" value="wph_save_settings">
<table class="form-table">
<tr>
<th><label for="wph-host">Gitea host</label></th>
<td>
<input id="wph-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>WPH_GITEA_HOST</code> constant.</p><?php endif; ?>
</td>
</tr>
<tr>
<th><label for="wph-owner">Owner</label></th>
<td>
<input id="wph-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>WPH_GITEA_OWNER</code> constant.</p><?php endif; ?>
</td>
</tr>
<tr>
<th><label for="wph-repo">Repo</label></th>
<td>
<input id="wph-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>WPH_GITEA_REPO</code> constant.</p><?php endif; ?>
</td>
</tr>
<tr>
<th><label for="wph-token">Token</label></th>
<td>
<input id="wph-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>WPH_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 (WPH_Recovery_Installer::is_configured() && !WPH_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('wph_recovery_install'); ?>
<input type="hidden" name="action" value="wph_recovery_install">
<button class="button button-primary">Install from gitea (latest)</button>
</form>
<?php elseif (WPH_Recovery_Bootstrap::is_installed()): ?>
<hr>
<p class="wph-ok">✓ Site Recovery is already installed at <code><?php echo esc_html((string) WPH_Recovery_Bootstrap::plugin_file()); ?></code>.</p>
<?php endif; ?>
</div>
</div>
<?php
}
function wph_handle_save_settings(): void {
if (!current_user_can('manage_options')) wp_die('Forbidden');
check_admin_referer('wph_save_settings');
WPH_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=site-healthcheck-settings&wph_saved=1'));
exit;
}
function wph_handle_recovery_install(): void {
if (!current_user_can('install_plugins') || !current_user_can('activate_plugins')) wp_die('Forbidden');
check_admin_referer('wph_recovery_install');
@set_time_limit(120);
$result = WPH_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=site-healthcheck-settings')) . '">Back to settings</a></p>');
}
set_transient('wph_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=site-healthcheck'));
exit;
}
function wph_handle_step_action(): void {
if (!current_user_can('manage_options')) wp_die('Forbidden');
$step_id = isset($_POST['step']) ? sanitize_key((string) $_POST['step']) : '';