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:
2026-07-07 14:54:15 +01:00
parent 1a9e5ac0fd
commit 3d67b94896
3 changed files with 111 additions and 18 deletions

View File

@@ -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);
}