Doctor_Command
in package
`wp creacaptcha doctor`
Table of Contents
Methods
- __invoke() : void
- Runs the diagnostic checklist and prints a status table.
- cloudflare_cron_check() : array{check: string, status: string, meldung: string}
- Check 4 — Cloudflare-Refresh-Cron.
-
filtered_interceptor_lists()
: array{paths: array
, actions: array } - The interceptor's own guard lists, run through the SAME filters `Interceptor::is_guarded()`/`action_patterns()` apply at runtime (`creationell_captcha_interceptor_paths` / `_actions`).
- gd_check() : array{check: string, status: string, meldung: string}
- Check 10 — PHP-GD-Extension (für Code-Challenge erforderlich).
- hmac_secrets_check() : array{check: string, status: string, meldung: string}
- Check 2 — HMAC-Secrets.
- inject_pattern_covered() : bool
- Check 15 — whether a single inject-path pattern has a real counterpart in the guard list.
- login_coverage_check() : array{check: string, status: string, meldung: string}
- Check 11 — Login-Formular-Abdeckung.
- login_coverage_row() : array{check: string, status: string, meldung: string}
- Ableitung zu Check 11 — B-I7.
- mapped_cidr_check() : array{check: string, status: string, meldung: string}
- Check 18 — CIDR-Einträge in IPv4-mapped-Notation (Cross-Strang-Hinweis F1).
- mapped_remote_addr_check() : array{check: string, status: string, meldung: string}
- Check 17 — IPv4-mapped REMOTE_ADDR (Cross-Strang-Hinweis F1, Bündel 4/I2).
- neutralising_excludes() : array<int, string>
- Returns the `!`-exclusion patterns of a list that on their own match every positive pattern of the same list — i.e. that neutralise it.
- probe_paths() : array<int, string>
- Turns a wildcard pattern into concrete sample strings it covers.
- proxy_header_choice_check() : array{check: string, status: string, meldung: string}
- Check 20 — Proxy-Header-Auswahl (Cross-Strang-Hinweis N1, "m2").
- retention_sweep_check() : array{check: string, status: string, meldung: string}
- Check 22 — läuft die Aufbewahrungsfrist des Event-Logs überhaupt?
- sanitized_snapshot() : array<string, mixed>
- Ableitung zu Check 9 / CLI-10 — der sanitisierte Vergleichsstand.
- secret_resolvable() : bool
- Ableitung zu Check 2 und Check 19 — löst EIN Secret aus seinen beiden Quellen auf, ohne die Getter zu rufen.
- settings_defaults_check() : array{check: string, status: string, meldung: string}
- Check 9 — Settings-Defaults-Vollstaendigkeit + CLI-10-Wertspezifikation.
- sodium_check() : array{check: string, status: string, meldung: string}
- Check 7b — Sodium-Extension (für Argon2id-Algorithmus erforderlich).
- underattack_cache_check() : array{check: string, status: string, meldung: string}
- Check 21 — Under-Attack-Interstitial und Full-Page-Cache (Cross-Strang-Hinweis N1, "m7", §3.3 — nennt alle drei hier verwendeten Signale ausdrücklich).
- underattack_pass_rate_check() : array{check: string, status: string, meldung: string}
- Check 19 — Under-Attack-Pass-Rate (Cross-Strang-Hinweis N1, "m4"/"m7").
- is_mapped_cidr() : bool
- Whether a CIDR range's subnet part is written in IPv4-mapped notation (`::ffff:a.b.c.d/N` — RFC 4291 §2.5.5.2), the one shape `creationell_captcha_normalize_ip()` deliberately does not touch (see Check 18's docblock). Mirrors that function's binary-form check, scoped to the subnet segment of a CIDR entry instead of a plain address.
Methods
__invoke()
Runs the diagnostic checklist and prints a status table.
public
__invoke(array<int, string> $args, array<string, string> $assoc_args) : void
Exit code 1 if at least one check is error, 0 otherwise.
Grenze des Werkzeugs (CLI-10): doctor liest ausschließlich — es prüft
Vorhandensein und Erreichbarkeit (Datei, Option, Tabelle, Cron, Extension)
sowie einige Konsistenzen zwischen Schaltern. Seit der Nachlese vergleicht
Check 9 den gespeicherten Wertesatz zusätzlich gegen den Sanitizer (rein
lesend, siehe dort) und erkennt damit einen an den Plugin-Schreibwegen
vorbei gesetzten Wert, der von der Feldspezifikation abweicht
(algorithm=boguswert per wp option patch). Nicht geprüft bleibt, ob
ein Wert innerhalb seiner Spezifikation auch inhaltlich sinnvoll ist
(ein syntaktisch gültiges, aber unerreichbares Cloudflare-IP-Ziel o. Ä.).
„status: ok" heißt deshalb „dieser Check hat nichts gefunden", nicht
„die Installation ist korrekt konfiguriert".
Third-party modules can extend the list via the
creationell_captcha_doctor_checks filter. Filter receives the array
of checks (each [check => string, status => 'ok'|'warn'|'error', meldung => string]) and must return the same shape.
OPTIONS
[--format=
EXAMPLES
wp creacaptcha doctor
Parameters
- $args : array<int, string>
-
Positional arguments.
- $assoc_args : array<string, string>
-
Associative arguments.
Tags
cloudflare_cron_check()
Check 4 — Cloudflare-Refresh-Cron.
public
static cloudflare_cron_check(array<string, mixed> $settings, int|false $next_scheduled) : array{check: string, status: string, meldung: string}
B-I4: the auto-refresh cron slot is only ever scheduled when BOTH
firewall_trust_cloudflare AND firewall_cloudflare_auto_refresh are
on (creationell_captcha_sync_cloudflare_cron(),
cloudflare-proxies.php:173-176) — reading firewall_cloudflare_auto_refresh
alone made this check report a permanent error on a healthy install
whose proxy mode is off but whose two dependent fields are still frozen
true by the settings requires-rewrite (they are simply gated, not
broken). Mirrors the exact condition sync_cloudflare_cron() uses, and
downgrades the "gated" case to warn — the field not taking effect is
expected, not a defect.
Parameters
- $settings : array<string, mixed>
-
Current plugin settings.
- $next_scheduled : int|false
-
Return value of
wp_next_scheduled('creationell_captcha_refresh_cloudflare_ips').
Return values
array{check: string, status: string, meldung: string}filtered_interceptor_lists()
The interceptor's own guard lists, run through the SAME filters `Interceptor::is_guarded()`/`action_patterns()` apply at runtime (`creationell_captcha_interceptor_paths` / `_actions`).
public
static filtered_interceptor_lists(array<string, mixed> $settings) : array{paths: array, actions: array}
W1-9: checks 14 and 15 used to read $settings['interceptor_paths']/
['interceptor_actions'] directly. That is not what the interceptor
itself decides on — a site using the documented
creationell_captcha_protect_path() developer API only ever touches
these filters, never the settings option, so the raw read was blind to
filter-added guards (false "uncovered"/"ok, no exclusion problem") and
equally blind to a filter that REMOVES a settings entry.
Public and static so the filter wiring itself — not just what the
downstream checks do with the result — can be exercised directly in
tests/test-cli-doctor-checks.php.
Parameters
- $settings : array<string, mixed>
-
Current plugin settings.
Return values
array{paths: arraygd_check()
Check 10 — PHP-GD-Extension (für Code-Challenge erforderlich).
public
static gd_check(array<string, mixed> $settings, bool $gd_loaded) : array{check: string, status: string, meldung: string}
B-I5: fehlendes GD lässt should_issue_code_challenge()
(code-challenge.php:31-33) die Zusatzstufe grundsätzlich nie auslösen —
die PoW-Stufe bleibt unverändert aktiv, nichts fällt aus. Working
degradation statt Ausfall, also warn statt error (siehe Check 16).
Parameters
- $settings : array<string, mixed>
-
Current plugin settings.
- $gd_loaded : bool
-
Result of
extension_loaded('gd').
Return values
array{check: string, status: string, meldung: string}hmac_secrets_check()
Check 2 — HMAC-Secrets.
public
static hmac_secrets_check(bool $sig_resolved, bool $key_resolved) : array{check: string, status: string, meldung: string}
B-M22: signature and key-signature each resolve independently — a site can set
one wp-config constant and rely on the stored option for the other. Missing
either one leaves the engine unable to sign/verify challenges, so error (not
warn) is correct here, matching the error-Regel in Check 16's comment.
Parameters
- $sig_resolved : bool
-
Whether the signature secret resolves (constant or option).
- $key_resolved : bool
-
Whether the key-signature secret resolves (constant or option).
Return values
array{check: string, status: string, meldung: string}inject_pattern_covered()
Check 15 — whether a single inject-path pattern has a real counterpart in the guard list.
public
static inject_pattern_covered(string $pattern, array<int, string> $guard_paths) : bool
Fixes two bugs found for this check (Bündel 5 / W1-10, W1-15):
- B-I6/W1-10: the previous loop set
$covered = trueon the FIRST matching probe and broke — an existence quantor answering "is ANY request under this pattern protected?" where the check's own claim ("jeder Inject-Pfad hat ein Gegenstück") requires a universal quantor.neutralising_excludes()right above already gets this direction correct for its own probes; this method aligns with it: ALL probes fromprobe_paths()must be covered. - W1-15: matching only
[rawurldecode($probe), $probe]covers the "inject pattern written percent-encoded" direction but not the reverse ("inject pattern written decoded, guard pattern written percent-encoded") —rawurldecode()on an already-decoded string is a no-op, so the guard's percent-encoded spelling was never produced to compare against. Building a third candidate — the probe percent-re-encoded segment-by-segment (preserving/) — covers that direction too, matching what a real browser sends over the wire for a non-ASCII path.
Public and static — like Interceptor::match_path() — so the three
fixed constellations (Erst-Treffer, beide Kodierungsrichtungen) can be
exercised directly in tests/test-cli-doctor-checks.php without
bootstrapping the whole WP-CLI command.
Parameters
- $pattern : string
-
One
interceptor_inject_pathspattern (no leading!). - $guard_paths : array<int, string>
-
Filtered
interceptor_pathspatterns (seeapply_filters()call in__invoke()).
Return values
boollogin_coverage_check()
Check 11 — Login-Formular-Abdeckung.
public
static login_coverage_check(array<string, mixed> $settings, bool $woocommerce_installed, bool $wc_login_active) : array{check: string, status: string, meldung: string}
B-I7: reading protect_wc_login raw ignored the kill switch and the
protect_woocommerce master toggle — a value frozen true by the
settings requires-rewrite from a time when Woo protection was on kept
reporting "ok" long after protect_woocommerce was switched off, hiding
the exact IN-7 gap (My-Account login unprotected) this check exists to
surface. creationell_captcha_wc_login_active() is the single source
of truth the render/verify hooks themselves use.
Parameters
- $settings : array<string, mixed>
-
Current plugin settings.
- $woocommerce_installed : bool
-
Result of
class_exists('WooCommerce'). - $wc_login_active : bool
-
Result of
creationell_captcha_wc_login_active().
Return values
array{check: string, status: string, meldung: string}login_coverage_row()
Ableitung zu Check 11 — B-I7.
public
static login_coverage_row(array<string, mixed> $settings) : array{check: string, status: string, meldung: string}
Die Substanz des B-I7-Fixes ist die QUELLE des dritten Arguments:
creationell_captcha_wc_login_active() statt des rohen Toggles
$settings['protect_wc_login']. Genau diese Wahl lag inline in run()
und war deshalb von keiner Suite bewacht — login_coverage_check() selbst
bekommt den Wert ja fertig gereicht. Als eigener Helfer ist der
eingefrorene Fall (protect_wc_login = true bei ausgeschaltetem Master
protect_woocommerce, AF-1) direkt pruefbar.
Parameters
- $settings : array<string, mixed>
-
Aktueller Wertesatz.
Return values
array{check: string, status: string, meldung: string}mapped_cidr_check()
Check 18 — CIDR-Einträge in IPv4-mapped-Notation (Cross-Strang-Hinweis F1).
public
static mapped_cidr_check(array<string, mixed> $settings) : array{check: string, status: string, meldung: string}
creationell_captcha_normalize_ip() (helpers.php) kanonisiert bewusst nur
einzelne Adressen, nie CIDR-Bereiche — ein Umrechnen der Präfixlänge könnte
eine Trusted-Proxy-Zeile stillschweigend verbreitern. Ein vor 1.1.0 in mapped
Notation eingetragener Bereich (::ffff:203.0.113.0/120) trifft seit 1.1.0
deshalb nicht mehr; dieser Check macht das sichtbar statt es still wirkungslos
werden zu lassen.
Parameters
- $settings : array<string, mixed>
-
Current plugin settings.
Return values
array{check: string, status: string, meldung: string}mapped_remote_addr_check()
Check 17 — IPv4-mapped REMOTE_ADDR (Cross-Strang-Hinweis F1, Bündel 4/I2).
public
static mapped_remote_addr_check(string $live_remote_addr, string|null $logged_mapped_ip) : array{check: string, status: string, meldung: string}
Zwei unabhängige Signale, weil ein wp-Aufruf über die CLI-SAPI normalerweise
KEINE echte HTTP-Verbindung hat: $_SERVER['REMOTE_ADDR'] ist unter WP-CLI
meist gar nicht gesetzt, ein Live-Treffer also nur bei ungewöhnlichen
Aufrufkontexten möglich. Das Event-Log ist die verlässlichere Quelle: da
creationell_captcha_get_client_ip() seit 1.1.0 jede IP kanonisiert, BEVOR sie
geloggt wird, kann eine ::ffff:-Adresse im Log nur aus der Zeit VOR dem
Update stammen — ein direkter, installationsspezifischer Nachweis, dass dieser
Server REMOTE_ADDR mindestens zeitweise mapped ausliefert.
Parameters
- $live_remote_addr : string
-
$_SERVER['REMOTE_ADDR']bei diesem Aufruf, oder ''. - $logged_mapped_ip : string|null
-
Eine im Event-Log gefundene mapped-Adresse, oder null.
Return values
array{check: string, status: string, meldung: string}neutralising_excludes()
Returns the `!`-exclusion patterns of a list that on their own match every positive pattern of the same list — i.e. that neutralise it.
public
static neutralising_excludes(array<int, mixed> $patterns) : array<int, string>
Probing with concrete sample paths derived from the positive patterns (rather than comparing pattern strings) uses the real matcher, so the answer is exactly the runtime behaviour.
B-M23: public (like inject_pattern_covered() above) so Check 14's own
matching — not just what the check does with the result — has a direct
Ebene-2 test in tests/test-cli-doctor-checks.php. Before this, the
~300-line doctor addition from Modul 27 had no such coverage at all for the
BK-15 exclusion logic; class-cli-list-command.php:339 makes the identical
visibility argument for a comparable validation seam.
Parameters
- $patterns : array<int, mixed>
-
Raw pattern list from the settings.
Return values
array<int, string> —Offending !-patterns, original spelling.
probe_paths()
Turns a wildcard pattern into concrete sample strings it covers.
public
static probe_paths(string $pattern) : array<int, string>
Two probes per pattern — * expanded to nothing and to a filler — so a
narrow exclusion that only catches one of them is not mistaken for one
that swallows the whole pattern.
B-M23: public for the same reason as neutralising_excludes() above.
Parameters
- $pattern : string
-
Wildcard pattern (without a leading
!).
Return values
array<int, string>proxy_header_choice_check()
Check 20 — Proxy-Header-Auswahl (Cross-Strang-Hinweis N1, "m2").
public
static proxy_header_choice_check(array<string, mixed> $settings) : array{check: string, status: string, meldung: string}
The four values mirror the firewall_proxy_header select field's options in
includes/settings.php (and creationell_captcha_get_client_ip()'s
$header_map in includes/helpers.php) — kept in sync manually since this
check must not depend on either of those files loading.
Parameters
- $settings : array<string, mixed>
-
Current plugin settings.
Return values
array{check: string, status: string, meldung: string}retention_sweep_check()
Check 22 — läuft die Aufbewahrungsfrist des Event-Logs überhaupt?
public
static retention_sweep_check(bool $disabled, bool $table_exists, int $retention_days) : array{check: string, status: string, meldung: string}
Nachlese N6, Befund 6. Analytics::run_scheduled_prune() und
ensure_prune_schedule() steigen seit W3-2 beim Kill-Switch sofort aus.
Für den kurzen Einsatz ist das richtig — Zeilen zu löschen ist die
einzige zerstörende Hintergrundoperation dieses Plugins, und wer während
eines Vorfalls CREATIONELL_CAPTCHA_DISABLE setzt, will den Zustand
einfrieren, nicht die Beweise wegräumen lassen. Für eine Konstante, die
länger steht als die Aufbewahrungsdauer, ist es eine offene Zusage: die
Einstellung sagt „ältere Einträge werden entfernt", und es passiert
nichts. Der Guard bleibt (dieselbe Entscheidung wie in W3-2), aber der
Zustand wird gemeldet statt still zu bleiben.
wp creacaptcha log prune prüft den Kill-Switch NICHT und ist damit der
Weg heraus, ohne die Konstante anzufassen — die Meldung nennt ihn.
Parameters
- $disabled : bool
-
Whether the wp-config kill switch is set.
- $table_exists : bool
-
Whether the event-log table is present.
- $retention_days : int
-
Configured retention window in days.
Return values
array{check: string, status: string, meldung: string}sanitized_snapshot()
Ableitung zu Check 9 / CLI-10 — der sanitisierte Vergleichsstand.
public
static sanitized_snapshot(array<string, mixed> $stored) : array<string, mixed>
Ruft den Sanitizer im PROGRAMMATIC-Kontext NUR zum Vergleichen auf und schreibt das Ergebnis nicht. Seit Welle 1 ist der Sanitizer seiteneffektfrei (der ensure_table()-Aufruf haengt an den update_option_/add_option_-Hooks, nicht mehr am Sanitizer), sonst waere dieser Aufruf aus einer rein lesenden Diagnose heraus unzulaessig.
Eigener Helfer aus demselben Grund wie secret_resolvable(): stand diese
Zeile inline in run(), liess sich der Wertvergleich auf $sanitized = $stored zuruecknehmen, ohne dass eine Suite rot wurde — Check 9 haette
dann strukturell nie eine Wertabweichung sehen koennen.
Parameters
- $stored : array<string, mixed>
-
Gespeicherter Wertesatz.
Return values
array<string, mixed> —Der sanitisierte Wertesatz.
secret_resolvable()
Ableitung zu Check 2 und Check 19 — löst EIN Secret aus seinen beiden Quellen auf, ohne die Getter zu rufen.
public
static secret_resolvable(string $constant, mixed $secrets_option, string $option_key) : bool
Bis zum Re-Review stand diese Disjunktion inline in run(); die Doctor-Suite ruft aber ausschliesslich die reinen Check-Funktionen mit bereits fertigen Argumenten auf, sodass eine Ruecknahme des Konstanten-Zweigs von KEINEM Test bemerkt wurde (Fehlerklasse 2: der Fix stimmt, der Beweis trifft den Pfad nicht). Als eigener Helfer ist die B-M22-Divergenz — Konstante gesetzt, Option leer — direkt und diskriminierend pruefbar.
Der Konstantenname kommt als Zeichenkette herein, damit ein Test beide
Zweige im selben Prozess durchlaufen kann; constant() liest ihn erst
nach dem defined()-Guard.
Bewusst NICHT ueber creationell_captcha_get_hmac_secret(): der Getter heilt eine fehlende Option durch Neuerzeugen und PERSISTIEREN — ein Schreibvorgang, den diese rein lesende Diagnose nie ausloesen darf (Vertrag „doctor liest ausschliesslich" im Klassen-Docblock).
Parameters
- $constant : string
-
Name der wp-config-Konstante (Vorrang).
- $secrets_option : mixed
-
Inhalt der Option
creationell_captcha_secrets(Rueckfall). - $option_key : string
-
Schluessel innerhalb dieser Option.
Return values
boolsettings_defaults_check()
Check 9 — Settings-Defaults-Vollstaendigkeit + CLI-10-Wertspezifikation.
public
static settings_defaults_check(array<string, mixed> $defaults, array<string, mixed> $stored, array<string, mixed> $sanitized) : array{check: string, status: string, meldung: string}
Nachlese N2: schliesst die CLI-10-Grenze ("nur die Schluesselmenge"), seit der
Sanitizer-Aufruf selbst kein DDL mehr ausloest (Welle 1). $sanitized ist der
Aufrufer-seitige, rein lesende Sanitizer-Durchlauf ueber $stored
(creationell_captcha_sanitize_settings($stored, CREATIONELL_CAPTCHA_SANITIZE_PROGRAMMATIC))
— diese Methode selbst ruft ihn nicht auf und bleibt dadurch ohne jede
settings.php-Abhaengigkeit direkt testbar.
Parameters
- $defaults : array<string, mixed>
-
creationell_captcha_get_default_settings(). - $stored : array<string, mixed>
-
Roher Optionswert.
- $sanitized : array<string, mixed>
-
$storeddurch den Sanitizer geschickt (PROGRAMMATIC, nicht geschrieben).
Return values
array{check: string, status: string, meldung: string}sodium_check()
Check 7b — Sodium-Extension (für Argon2id-Algorithmus erforderlich).
public
static sodium_check(array<string, mixed> $settings, bool $sodium_available) : array{check: string, status: string, meldung: string}
B-I5: Engine::algorithm_key() (class-engine.php:601-611) falls back to
PBKDF2 silently and logs when ext-sodium is missing — challenges keep
working. That is a working degradation, not the "cannot work" state
error is reserved for (see the Check-16 comment below), so this is
warn.
Parameters
- $settings : array<string, mixed>
-
Current plugin settings.
- $sodium_available : bool
-
Result of
creationell_captcha_sodium_available().
Return values
array{check: string, status: string, meldung: string}underattack_cache_check()
Check 21 — Under-Attack-Interstitial und Full-Page-Cache (Cross-Strang-Hinweis N1, "m7", §3.3 — nennt alle drei hier verwendeten Signale ausdrücklich).
public
static underattack_cache_check(array<int, string> $active_cache_plugins, bool $wp_cache_constant, bool $advanced_cache_dropin) : array{check: string, status: string, meldung: string}
Parameters
- $active_cache_plugins : array<int, string>
-
Labels of active known caching plugins.
- $wp_cache_constant : bool
-
Whether
WP_CACHEis defined and truthy. - $advanced_cache_dropin : bool
-
Whether
wp-content/advanced-cache.phpexists.
Return values
array{check: string, status: string, meldung: string}underattack_pass_rate_check()
Check 19 — Under-Attack-Pass-Rate (Cross-Strang-Hinweis N1, "m4"/"m7").
public
static underattack_pass_rate_check(bool $sig_resolvable, array{attempts: int, passed: int}|null $data) : array{check: string, status: string, meldung: string}
Two independent signals, checked in order of confidence:
$sig_resolvablefalse — no HMAC signature secret resolves (neither the wp-config constant nor the stored option), somint_pass()cannot derive a token for ANY visitor. This is N1's suggested direct condition (creationell_captcha_derive_hmac_key(...) === ''), reformulated without calling the self-healing getter chain (see the__invoke()comment above this call) —error, because the site is durably unusable, matching theerror-reservation rule from Check 16's comment.$data— the event-log symptom (see below), for the other cause (headers_sent()), which cannot be observed directly from a WP-CLI process.
$data is null when the prerequisites for a meaningful measurement are not
met (event log off, either log gate off, or the table missing) — reported as
"not measurable", never as "ok" by absence of evidence. Below the attempt
threshold the same applies: zero passes out of zero or a handful of attempts
says nothing about whether the gate is passable at all.
Parameters
- $sig_resolvable : bool
-
Whether the HMAC signature secret resolves (constant or option).
- $data : array{attempts: int, passed: int}|null
-
Event counts, or null if unmeasurable.
Return values
array{check: string, status: string, meldung: string}is_mapped_cidr()
Whether a CIDR range's subnet part is written in IPv4-mapped notation (`::ffff:a.b.c.d/N` — RFC 4291 §2.5.5.2), the one shape `creationell_captcha_normalize_ip()` deliberately does not touch (see Check 18's docblock). Mirrors that function's binary-form check, scoped to the subnet segment of a CIDR entry instead of a plain address.
private
static is_mapped_cidr(string $entry) : bool
Parameters
- $entry : string
-
One list entry, already trimmed.