Plugin: settings-screen fallback for API URL + API key (hc-4m5)
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>
This commit is contained in:
@@ -15,6 +15,7 @@ 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 {
|
||||
@@ -114,8 +115,9 @@ function att_hc_render_admin_page(): void {
|
||||
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>Add the following to <code>wp-config.php</code> and reload:</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;
|
||||
@@ -721,6 +723,44 @@ function att_hc_render_settings_page(): void {
|
||||
|
||||
<?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>
|
||||
@@ -793,6 +833,27 @@ function att_hc_handle_save_settings(): void {
|
||||
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');
|
||||
|
||||
@@ -4,25 +4,57 @@ if (!defined('ABSPATH')) exit;
|
||||
/**
|
||||
* HTTP client for the central healthcheck history server.
|
||||
*
|
||||
* Config comes from constants defined in wp-config.php:
|
||||
* Config comes from EITHER a wp-config.php constant OR a WP option, with the
|
||||
* constant winning when both are set:
|
||||
*
|
||||
* define('ATT_HC_API_URL', 'https://healthcheck-history.example.com');
|
||||
* define('ATT_HC_API_KEY', '<long random string>');
|
||||
*
|
||||
* Constants (not options) on purpose: the plugin gets uninstalled per engagement,
|
||||
* but the constants survive in wp-config.php so the next install on the same site
|
||||
* still talks to the same server.
|
||||
* — OR set via Tools → Site Healthcheck → Settings, which persists to the
|
||||
* WP options `att_hc_api_url` and `att_hc_api_key`.
|
||||
*
|
||||
* The constant path survives plugin uninstall + reactivation (values live in
|
||||
* wp-config.php on the filesystem). The option path is easier on managed hosts
|
||||
* where wp-config.php isn't editable, but the key is then visible to anyone
|
||||
* with WP admin or DB access — prefer the constant when possible.
|
||||
*
|
||||
* Every method throws ATT_HC_Api_Exception on failure. Callers must catch and surface.
|
||||
*/
|
||||
final class ATT_HC_Api {
|
||||
|
||||
public const OPT_URL = 'att_hc_api_url';
|
||||
public const OPT_KEY = 'att_hc_api_key';
|
||||
|
||||
private const TIMEOUT_SECONDS = 15;
|
||||
|
||||
/** Resolved API URL — constant wins, else option, else ''. */
|
||||
public static function url(): string {
|
||||
if (defined('ATT_HC_API_URL') && is_string(ATT_HC_API_URL) && ATT_HC_API_URL !== '') {
|
||||
return ATT_HC_API_URL;
|
||||
}
|
||||
return (string) get_option(self::OPT_URL, '');
|
||||
}
|
||||
|
||||
/** Resolved API key — constant wins, else option, else ''. */
|
||||
public static function key(): string {
|
||||
if (defined('ATT_HC_API_KEY') && is_string(ATT_HC_API_KEY) && ATT_HC_API_KEY !== '') {
|
||||
return ATT_HC_API_KEY;
|
||||
}
|
||||
return (string) get_option(self::OPT_KEY, '');
|
||||
}
|
||||
|
||||
/** True if the field is locked by a wp-config constant (used to disable inputs on the settings page). */
|
||||
public static function url_from_constant(): bool {
|
||||
return defined('ATT_HC_API_URL') && is_string(ATT_HC_API_URL) && ATT_HC_API_URL !== '';
|
||||
}
|
||||
|
||||
public static function key_from_constant(): bool {
|
||||
return defined('ATT_HC_API_KEY') && is_string(ATT_HC_API_KEY) && ATT_HC_API_KEY !== '';
|
||||
}
|
||||
|
||||
/** Returns true if the plugin is configured to talk to a server. */
|
||||
public static function is_configured(): bool {
|
||||
return defined('ATT_HC_API_URL') && defined('ATT_HC_API_KEY')
|
||||
&& is_string(ATT_HC_API_URL) && is_string(ATT_HC_API_KEY)
|
||||
&& ATT_HC_API_URL !== '' && ATT_HC_API_KEY !== '';
|
||||
return self::url() !== '' && self::key() !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -30,14 +62,13 @@ final class ATT_HC_Api {
|
||||
* Used by the admin page to block the UI with a clear error.
|
||||
*/
|
||||
public static function config_error(): ?string {
|
||||
if (!defined('ATT_HC_API_URL') || !defined('ATT_HC_API_KEY')) {
|
||||
return 'Central history server is not configured. Add ATT_HC_API_URL and ATT_HC_API_KEY constants to wp-config.php.';
|
||||
$url = self::url();
|
||||
$key = self::key();
|
||||
if ($url === '' || $key === '') {
|
||||
return 'Central history server is not configured. Set it via Tools → Site Healthcheck → Settings, or define ATT_HC_API_URL and ATT_HC_API_KEY constants in wp-config.php.';
|
||||
}
|
||||
if (!is_string(ATT_HC_API_URL) || !is_string(ATT_HC_API_KEY) || ATT_HC_API_URL === '' || ATT_HC_API_KEY === '') {
|
||||
return 'ATT_HC_API_URL or ATT_HC_API_KEY in wp-config.php is empty.';
|
||||
}
|
||||
if (stripos(ATT_HC_API_URL, 'https://') !== 0 && !self::is_loopback(ATT_HC_API_URL)) {
|
||||
return 'ATT_HC_API_URL must start with https:// — refusing to send credentials over plain HTTP.';
|
||||
if (stripos($url, 'https://') !== 0 && !self::is_loopback($url)) {
|
||||
return 'The central history server URL must start with https:// — refusing to send credentials over plain HTTP.';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -107,7 +138,7 @@ final class ATT_HC_Api {
|
||||
throw new ATT_HC_Api_Exception($err, 'no_config');
|
||||
}
|
||||
|
||||
$url = rtrim(ATT_HC_API_URL, '/') . $path;
|
||||
$url = rtrim(self::url(), '/') . $path;
|
||||
|
||||
$args = [
|
||||
'method' => $method,
|
||||
@@ -118,7 +149,7 @@ final class ATT_HC_Api {
|
||||
],
|
||||
];
|
||||
if ($requires_auth) {
|
||||
$args['headers']['Authorization'] = 'Bearer ' . ATT_HC_API_KEY;
|
||||
$args['headers']['Authorization'] = 'Bearer ' . self::key();
|
||||
}
|
||||
if ($body !== null) {
|
||||
$args['headers']['Content-Type'] = 'application/json';
|
||||
@@ -145,7 +176,7 @@ final class ATT_HC_Api {
|
||||
? $data['error']
|
||||
: 'central server returned HTTP ' . $status;
|
||||
if ($status === 401) {
|
||||
$message = 'Central server rejected our credentials. Check ATT_HC_API_KEY in wp-config.php matches the server config.';
|
||||
$message = 'Central server rejected our credentials. Check the API key (Tools → Site Healthcheck → Settings, or ATT_HC_API_KEY in wp-config.php) matches the server config.';
|
||||
}
|
||||
throw new ATT_HC_Api_Exception($message, $code, $status);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user