CREATIONELL_CAPTCHA_BASENAME
public
mixed
CREATIONELL_CAPTCHA_BASENAME
= \plugin_basename(__FILE__)
public
mixed
CREATIONELL_CAPTCHA_BASENAME
= \plugin_basename(__FILE__)
public
mixed
CREATIONELL_CAPTCHA_DEBUG
= \defined('WP_DEBUG') && \WP_DEBUG
The default upper bound on rows in a single CSV export.
public
mixed
CREATIONELL_CAPTCHA_EXPORT_MAX_ROWS
= 50000
Schema version of the settings-export format.
public
mixed
CREATIONELL_CAPTCHA_EXPORT_SCHEMA
= 1
public
mixed
CREATIONELL_CAPTCHA_FILE
= __FILE__
Query-Parameter des Ausblenden-Links.
public
mixed
CREATIONELL_CAPTCHA_HARDENING_DISMISS_ARG
= 'creationell_captcha_hardening_dismiss'
User-Meta-Schlüssel, unter dem ein Benutzer den Hinweis wegklickt.
public
mixed
CREATIONELL_CAPTCHA_HARDENING_DISMISS_META
= 'creationell_captcha_hardening_dismissed'
Maps WordPress locales to the matching ALTCHA i18n locale bundle.
public
mixed
CREATIONELL_CAPTCHA_LOCALE_MAP
= [
// Deutsch
'de_DE' => 'de',
'de_DE_formal' => 'de',
'de_AT' => 'de',
'de_CH' => 'de',
'de_CH_informal' => 'de',
// Englisch
'en_US' => 'en',
'en_GB' => 'en',
'en_AU' => 'en',
'en_CA' => 'en',
'en_NZ' => 'en',
'en_ZA' => 'en',
// Französisch
'fr_FR' => 'fr-fr',
'fr_BE' => 'fr-fr',
'fr_LU' => 'fr-fr',
'fr_CH' => 'fr-fr',
'fr_CA' => 'fr-ca',
// Spanisch (Europa)
'es_ES' => 'es-es',
// Spanisch (LatAm — alle nach es-419)
'es_AR' => 'es-419',
'es_CL' => 'es-419',
'es_CO' => 'es-419',
'es_CR' => 'es-419',
'es_DO' => 'es-419',
'es_EC' => 'es-419',
'es_GT' => 'es-419',
'es_HN' => 'es-419',
'es_MX' => 'es-419',
'es_PA' => 'es-419',
'es_PE' => 'es-419',
'es_PR' => 'es-419',
'es_UY' => 'es-419',
'es_VE' => 'es-419',
// Portugiesisch
'pt_PT' => 'pt-pt',
'pt_AO' => 'pt-pt',
'pt_BR' => 'pt-br',
// Italienisch / Niederländisch / Polnisch
'it_IT' => 'it',
'nl_NL' => 'nl',
'nl_BE' => 'nl',
'pl_PL' => 'pl',
// Tschechisch / Slowakisch
'cs_CZ' => 'cs',
'sk_SK' => 'sk',
// Nordisch
'da_DK' => 'da',
'sv_SE' => 'sv',
'fi' => 'fi',
'nb_NO' => 'nb',
'nn_NO' => 'nb',
// Sonstige EU
'hu_HU' => 'hu',
'ro_RO' => 'ro',
'el' => 'el',
]
Keys are values returned by get_locale() — including the formal /
informal variants WP exposes (de_DE_formal, de_CH_informal). Values
are the exact locale codes used by the vendored ALTCHA bundles under
assets/js/altcha-i18n/<code>.js. Locales not in the map fall through
to the widget's own auto-detection (which itself falls back to English
since only the bundles enqueued by this plugin are registered).
Extend via the creationell_captcha_widget_locale_map filter rather
than patching this constant.
public
mixed
CREATIONELL_CAPTCHA_MIN_PHP
= '8.3'
public
mixed
CREATIONELL_CAPTCHA_MIN_WP
= '6.9'
public
mixed
CREATIONELL_CAPTCHA_PLUGIN_PATH
= \plugin_dir_path(__FILE__)
public
mixed
CREATIONELL_CAPTCHA_PLUGIN_URL
= \plugin_dir_url(__FILE__)
Purpose label of the under-attack code-challenge suppression token (`ctx`).
public
mixed
CREATIONELL_CAPTCHA_PURPOSE_UA_CTX
= 'underattack-ctx'
Purpose label of the under-attack pass-cookie key.
public
mixed
CREATIONELL_CAPTCHA_PURPOSE_UA_PASS
= 'underattack-pass'
Sanitiser context: the settings form (`options.php`).
public
mixed
CREATIONELL_CAPTCHA_SANITIZE_FORM
= 'form'
Ein disabled-Input sendet beim Absenden keinen Wert — deshalb (und NUR
deshalb) schreibt der Sanitizer in diesem Kontext gegatete Felder aus dem
gespeicherten Zustand zurück.
Sanitiser context: programmatischer Schreibvorgang (Import, Werksreset, „Standardwerte laden", WP-CLI).
public
mixed
CREATIONELL_CAPTCHA_SANITIZE_PROGRAMMATIC
= 'programmatic'
Hier ist der übergebene Wert eine vollständige, bewusste Angabe; eine Rückschreibung aus dem Altzustand würde sie still überstimmen (AF-1/AF-3).
Sanitiser context: Werksreset (`creationell_captcha_reset_settings()`).
public
mixed
CREATIONELL_CAPTCHA_SANITIZE_RESET
= 'reset'
Wie PROGRAMMATIC — plus die eine Zusage, die den Werksreset von jedem anderen Schreibvorgang unterscheidet: KEIN gespeicherter Schlüssel wird übernommen, auch keiner ausserhalb der aktuellen Feldspezifikation.
Warum das ein eigener Wert sein muss (Nachlese N6, Befund 5): Die
Übernahme-Schleife am Ende von creationell_captcha_sanitize_settings()
hält gespeicherte Schlüssel fest, die creationell_captcha_settings_fields()
gerade nicht liefert — die Schalter der Formular-Plugins hängen an
class_exists(), sind bei deaktiviertem Plugin also nicht in der
Spezifikation. Für jeden regulären Schreibvorgang ist das richtig
(sonst löschte ein Speichern der Einstellungsseite den Schalter eines
gerade deaktivierten Plugins). Für den dokumentierten „vollen Werksreset"
ist es falsch: wp creacaptcha settings reset --yes meldete
„Auf Werkseinstellungen zurückgesetzt", während ein protect_wpforms => false
aus der Zeit vor der Deaktivierung — oder ein beliebiger Fremdschlüssel aus
wp option patch/DB-Restore — die Option unverändert überlebte.
Der Wert ersetzt PROGRAMMATIC ausschliesslich beim Reset. Import, „Standardwerte laden", WP-CLI-Schreibvorgänge und die Listen-Befehle bleiben auf PROGRAMMATIC: sie übergeben zwar ebenfalls einen vollständigen Wertesatz, sind aber kein Werksreset und dürfen den Schalter eines gerade inaktiven Formular-Plugins nicht wegräumen.
Lifetime (seconds) of a `ctx` suppression token. The interstitial widget fetches the challenge on load, i.e. within seconds of the 503 being rendered; two minutes is generous for that and replaces the ~10 minutes the old 5-minute-bucket pair accepted (BK-4).
public
mixed
CREATIONELL_CAPTCHA_UA_CTX_TTL
= 120
public
mixed
CREATIONELL_CAPTCHA_VERSION
= '1.1.2'
Registers the top-level "CreaCaptcha" admin menu entry.
creationell_captcha_register_admin_menu() : void
Renders the tabbed settings page.
creationell_captcha_render_settings_page() : void
All tabs share one
Renders the "an import replaces everything" warning on the Werkzeuge page.
creationell_captcha_render_import_warning() : void
AF-6: Der Import prüft nur den Envelope (plugin/type/schema), keine
Herkunft — das exportierte site_url wurde nie verglichen — und ungenannte
Schlüssel fallen auf ihren Default, ein Teilimport ist also ein Vollreset.
Das ist manage_options-gated und damit kein anonymer Vektor; statt
Signaturinfrastruktur bekommt der Vorgang eine Warnhinweis-Ebene: hier vor
dem Import, und nach dem Import benennt die Erfolgsmeldung die Herkunft der
Datei (siehe creationell_captcha_handle_import_settings()).
Renders the "proxy mode on but trust-set empty" admin notice on plugin pages.
creationell_captcha_render_trust_notice() : void
The notice is persistent (not dismissible) — it disappears automatically as soon as any trust source is configured.
Records and reports the admin-page hook suffixes of the plugin's tab-organised pages.
creationell_captcha_tabbed_page_hooks([mixed $add = null ]) : array<int, string>
A submenu page's hook suffix derives from the sanitised parent menu title, not its slug, so it cannot be reliably hardcoded. Each tabbed page passes the value WordPress returns from add_menu_page()/add_submenu_page() here, and the admin asset loader matches the current screen against the recorded set.
Hook suffix to record; ignored unless a non-empty string.
All recorded hook suffixes.
Determines the active tab from the request, whitelisted against $tabs.
creationell_captcha_active_tab(array<string, string> $tabs) : string
Falls back to the first tab when no valid tab query parameter is present.
The value only selects which panel is shown and is strictly whitelisted
against the given registry, so no nonce check is required.
Tab registry (id => label).
Active tab id.
Renders the no-JavaScript fallback style and the nav-tab bar.
creationell_captcha_render_nav_tabs(array<string, string> $tabs, string $active_tab, string $base_url) : void
Without JavaScript the per-tab panels would each be hidden by admin.css; the
Tab registry (id => label).
Active tab id.
Page URL the tab links point at.
Neutralises a CSV cell against spreadsheet formula injection.
creationell_captcha_csv_cell(string $value) : string
A value beginning with =, +, - or @ can be executed as a formula by Excel or
LibreOffice; a leading single quote forces the spreadsheet to treat the value
as text. The path, user_agent, referrer, verification_data and
request_body columns all carry bytes an anonymous sender chose.
B-M10: TAB and CR belong in the same list. Both are stripped as leading
whitespace while the cell is parsed, so \treq(...) and \r=1+1 reach the
formula parser as if the control character were not there — the value is
then executed exactly like the four printable starters above. OWASP names
them alongside; they were the two the original list missed.
NOT covered, deliberately: a value whose formula starter sits behind other text, and a cell that is dangerous only inside a quoted field. The first is not a formula for any spreadsheet, the second is handled by fputcsv()'s quoting. What this function promises is exactly "the first character cannot start a formula".
Raw cell value.
Streams the filtered event log as a CSV download.
creationell_captcha_export_events() : void
Hooked to admin-post.php. Requires the manage_options capability and a
valid nonce. The filter (search, event type, date range) is read from the
request via the shared parser, so the export mirrors the on-screen filter.
Every reason to refuse is checked BEFORE the first byte goes out. Once the Content-Disposition header and the BOM are on the wire the response is committed to being a successful download, and anything that goes wrong afterwards reaches the admin as a file that looks complete (AF-10/DS-9).
Registers the "Statistik" submenu page under the CreaCaptcha menu.
creationell_captcha_register_analytics_page() : void
Human-readable labels for the seven event types.
creationell_captcha_analytics_labels() : array<string, string>
Thematic groups for the overview tab: title, explanation and the member event types with their short in-group labels.
creationell_captcha_analytics_groups() : array<int, array{title: string, description: string, types: array}>
Together the groups cover all seven event types exactly once. The long labels from creationell_captcha_analytics_labels() stay untouched for the filter dropdown, CSV export, CLI and history tab.
The three tabs of the analytics page.
creationell_captcha_analytics_tabs() : array<string, string>
Tab id => visible label.
Sums the $days most recent day buckets, per event type.
creationell_captcha_sum_recent_days(array<string, array<string, int>> $daily, int $days) : array<string, int>
Iterates today plus the ($days - 1) preceding days — the same window the 30-day history table walks.
Daily counters keyed by 'Y-m-d'.
Number of day buckets to sum.
Event type => sum.
Sums the $hours most recent hour buckets, per event type.
creationell_captcha_sum_recent_hours(array<string, array<string, int>> $hourly, int $hours) : array<string, int>
Iterates the current hour plus the ($hours - 1) preceding hours.
Hourly counters keyed by 'Y-m-d H'.
Number of hour buckets to sum.
Event type => sum.
Renders the tabbed analytics dashboard page.
creationell_captcha_render_analytics_page() : void
Renders the "Übersicht" tab: four 24-hour KPI tiles and one four-window comparison table per thematic group.
creationell_captcha_render_analytics_overview() : void
Renders the "Verlauf" tab: the day-by-day table for the last 30 days.
creationell_captcha_render_analytics_history() : void
Reads and sanitises the event-log filter from the request.
creationell_captcha_events_query_args() : array{search: string, event_type: string, date_from: string, date_to: string}
Used by the "Ereignisse" tab and by the CSV export handler. The values only narrow a read-only query and are bound via $wpdb->prepare downstream, so no nonce check is required here; the page number is read separately by the tab.
Renders the "Ereignisse" tab: filter toolbar, the event table with a detail link per row, the embedded event data and the detail modal.
creationell_captcha_render_analytics_events() : void
Renders the (initially hidden) event-detail modal skeleton.
creationell_captcha_render_event_modal() : void
The value cells carry a data-field matching the event record key; the modal
JavaScript fills them client-side from the embedded JSON map.
Renders the event-log filter toolbar: search, type filter, date range, the "Filtern"/"Zurücksetzen" controls and the CSV export link.
creationell_captcha_render_events_toolbar(array{search: string, event_type: string, date_from: string, date_to: string} $filter) : void
Active filter.
Renders the pagination navigation below the event-log table.
creationell_captcha_render_events_pagination(array{search: string, event_type: string, date_from: string, date_to: string} $filter, int $paged, int $total_pages) : void
Active filter.
Current page (1-based).
Total page count.
Returns the shared analytics recorder instance.
creationell_captcha_analytics() : Analytics
Records a security event of the given type.
creationell_captcha_record_event(string $type[, array<string, mixed> $context = [] ]) : void
The event type.
Optional caller-supplied context.
Enqueues admin styles and scripts on the CreaCaptcha admin pages.
creationell_captcha_enqueue_admin_assets(string $hook_suffix) : void
Current admin page hook suffix.
Returns the bundled Cloudflare snapshot.
creationell_captcha_cloudflare_snapshot() : array{v4: string[], v6: string[], updated_at: string}
The active CF range list: cached option (if fresh and valid) → bundled snapshot.
creationell_captcha_cloudflare_ranges() : array<string|int, string>
The cached option is considered stale once it is older than 48 hours, shielding against a silently broken cron job.
Every entry is re-validated on read. The refresh path already filters what
it fetches, but the option is a persisted value: it can predate the day
is_valid_ip_or_cidr() started rejecting a prefix length of 0, and it can be
written from outside this file (wp option update, a restored backup). One
0.0.0.0/0 in there turns every peer into a trusted proxy, which makes
X-Forwarded-For — and with it the client IP the whole bypass chain runs on
— spoofable (LK-13). The validator is the one in helpers.php, deliberately
not a second copy of the rule.
A cached list that does not survive validation is not partially used: the bundled snapshot is the safer answer than a list somebody has been editing.
IPv4 and IPv6 CIDR ranges, merged.
Fetches the live Cloudflare ranges and writes them to the cache option.
creationell_captcha_refresh_cloudflare_ips_now() : array{ok: bool, v4: int, v6: int, fetched_at: int|null, error: string|null}
Deletes the cached Cloudflare-range option. The next read falls back to the bundled snapshot. Returns true when the option existed and was deleted, false when the option was absent or the delete failed.
creationell_captcha_clear_cloudflare_cache() : bool
Fetches one Cloudflare endpoint and returns the valid CIDR entries.
creationell_captcha_fetch_cloudflare_list(string $url) : array<string|int, string>
Ensures the daily refresh cron slot is in sync with the auto-refresh toggle.
creationell_captcha_sync_cloudflare_cron() : void
Hooked on update_option_creationell_captcha_settings (fires on every
settings save). Deactivation cleanup is handled explicitly in
creationell_captcha_deactivate() to avoid re-scheduling the slot during
the deactivation handler.
Probes whether GD can actually render text with the vendored TTF font.
creationell_captcha_code_challenge_font_usable() : bool
is_file() alone is not enough (ZP-2): a corrupted font or one swapped for
an unrelated/unreadable file still passes that check, but every
imagettftext() call against it then silently returns false and draws
nothing — the renderer would produce a blank white PNG with no error and
no log line. This probe draws into a throwaway 1-character canvas and
reports the real imagettftext() verdict, so both the renderer and
wp creacaptcha doctor (Check „Code-Challenge-Schriftart") can detect the
defect before a visitor ever requests /code-image.
A corrupted TTF fails identically for every glyph (the failure is a freetype parse error on the file, not a missing character), so probing with a single throwaway character reflects the file's real state.
Renders a PNG of the given code and returns the raw bytes. Caller is responsible for emitting headers (`Content-Type: image/png`, `Cache-Control: no-store`) and the body.
creationell_captcha_render_code_image(string $code) : string
Falls back to GD's built-in bitmap font 5 if the vendored TTF is missing
OR present but unusable (ZP-2: corrupted/swapped file — is_file() alone
cannot see that) — either way logs a one-line warning so the operator sees
the degradation instead of silently shipping a blank image.
The code to render (4–8 chars expected; longer is trimmed implicitly by the width budget).
Decides whether the current /challenge request should attach a code-challenge instruction. Returns true iff:
creationell_captcha_should_issue_code_challenge() : bool
Returns the active charset string for the configured option.
creationell_captcha_code_charset(string $charset_key) : string
One of: digits / alphanumeric / alphanumeric-no-confusing.
Generates a fresh random code from the configured charset.
creationell_captcha_generate_code() : string
Issues a token backed by a server-side WP transient that stores the expected code for at most $expiry_seconds. Returns the opaque ID that `/code-image` and `/code-verify` use to look the code up again.
creationell_captcha_code_token_issue(string $code, int $expiry_seconds) : string
The code is intentionally NOT encoded into the token itself — a base64 round-trip would leak the code to anyone who can read the network response (defeats the OCR-resistant captcha goal). Server state via WP transients is the accepted trade-off.
The expected code (already from generator).
Seconds until the token expires.
Looks up the code for a token and returns it, or null on: - malformed token (not 32 hex chars) - missing transient (expired or unknown)
creationell_captcha_code_token_verify(string $token[, bool $consume = false ]) : string|null
The 32-hex-char ID from code_token_issue.
Delete the transient after successful lookup (single-use semantics). The /code-image handler passes false; /code-verify passes true ONLY after a successful code match so retries on wrong input still work.
Transient name of the per-token failed-attempt counter (CM-3).
creationell_captcha_code_fail_key(string $token) : string
The 32-hex-char ID from code_token_issue.
Records one wrong image-code submission for a token and invalidates the token once its attempt budget is spent.
creationell_captcha_code_token_fail(string $token) : void
Before this existed, a wrong code was completely free: /code-verify
returned 401 before the consume, nothing was counted, and the REST route
is not covered by the default rate-limit scopes (core/forms). A client
could therefore walk a token through the entire code space. The counter
lives exactly as long as the token it belongs to, so it cannot be reset by
waiting — only by fetching a fresh challenge, which also means a fresh PoW.
The 32-hex-char ID from code_token_issue.
Handles GET /code-image?t=<token>. Looks up the code from the token (server-side transient), renders the PNG, returns 410 on token failure.
creationell_captcha_rest_code_image(WP_REST_Request $request) : WP_REST_Response
Idempotent — the transient is NOT consumed here so the browser may reload the image.
The REST request. Required query parameter
t (opaque server-issued token).
200 with image/png body on success; 410 when the
token is unknown/expired or PHP-GD is unavailable.
Handles POST /code-verify. Two body shapes:
creationell_captcha_rest_code_verify(WP_REST_Request $request) : WP_REST_Response
Code-Challenge mode: { "code": "
verifyUrl ? _e() : verified()).
Structural verify only — nothing is consumed, because there is
nothing to protect: the endpoint hands back exactly what it was
given.Neither mode mints a payload any more (CM-5, Wurzel 3.1). The old handler
ended in Engine::issue_signed_payload(), which made the SERVER solve a
fresh PoW (solveChallenge()) and returned a ready-to-use payload — one
client-side solve was enough to drive an endless loop of server-side key
derivations. The response now echoes the SUBMITTED payload back verbatim,
so a caller never gains anything it did not already have.
The REST request with JSON body
{payload: string, code?: string}.
200 on success ({payload, verified: true}); 400 on malformed body; 401 on wrong code; 410 on missing/expired/unsigned payload or token.
Registers the two code-challenge REST routes. The /challenge handler itself stays in includes/rest.php; Task 11 extends it with the codeChallenge field and the data.ccode embed.
creationell_captcha_register_code_challenge_routes() : void
Registers email obfuscation on `init`. The decoder script is always registered; then, unless the kill-switch is set or the feature is off, the configured mode is wired up — the content filters (module 7) or the full-page output buffer (module 8).
creationell_captcha_register_email_obfuscation() : void
Wires up the full-page-buffer mode: on `template_redirect` for non-feed front-end requests it enqueues the decoder script and starts an output buffer whose callback obfuscates the page body at flush time.
creationell_captcha_register_email_buffer(EmailObfuscator $obfuscator) : void
Der Callback fasst den Puffer nur an, wenn die Antwort HTML ist (EO-5) und
eine Größengrenze nicht überschreitet (EO-4). Beides schützt fremde
Antworten davor, umgeschrieben zu werden, und verhindert, dass der
Obfuskator für eine sehr große Antwort eine zweite Kopie im Speicher
aufbaut. Was beides NICHT verhindert: dass PHPs Ausgabepuffer die Antwort
überhaupt zwischenspeichert. Ein Handler, der eine große Datei per
readfile() ausgibt, ohne die offenen Puffer vorher zu leeren, hält sie so
oder so komplett im Speicher — das ist eine Eigenschaft von ob_start()
und trifft jeden Puffer auf der Seite, nicht nur diesen.
Kein creationell_captcha_request_bypassed() (EO-10, bewusst): Die
Obfuskation ist kein Zugangs-Gate, sondern eine Darstellungsänderung — sie
lässt niemanden durch und hält niemanden auf. Der Content-Filter-Modus
fragt den Bypass ebenso wenig ab; ihn nur hier zu verdrahten, hieße, aus
einer Einstellung zwei Verhaltensweisen zu machen, je nach gewähltem Modus.
Funktionaler Schaden entsteht nicht: der Decoder stellt die Adresse
clientseitig wieder her.
The obfuscator.
The buffer callback itself: decides whether this response gets rewritten and hands it to the obfuscator when it does.
creationell_captcha_email_buffer_filter(string $html, EmailObfuscator $obfuscator) : string
Bewusst eine eigene benannte Funktion statt einer Closure im ob_start():
Die beiden Weichen (EO-5 Content-Type, EO-4 Größengrenze) sind der Fix, und
ein Test muss sie direkt aufrufen können. In der Closure waren sie nur über
einen echten Request erreichbar — der einzige „Nachweis", der dann noch
möglich war, prüfte den Default-Wert gegen sich selbst und wäre auch ohne die
Weiche grün geblieben.
Der gepufferte Antwort-Body.
The obfuscator.
Umgeschriebener oder unveränderter Body.
Whether the response being buffered is (still) an HTML document.
creationell_captcha_response_is_html() : bool
EO-5: process_page() prüfte nie den Content-Type. Eine Nicht-HTML-Antwort,
die zufällig <body…>, </body> und ein „@" enthielt — etwa ein von einem
Mu-Plugin auf template_redirect ausgegebenes JSON mit HTML-Fragment —,
wurde umgeschrieben und war danach kaputt.
Ein fehlender Content-Type gilt als HTML: template_redirect ist der
Template-Pfad von WordPress, dessen Antwort per Definition das Theme rendert,
und PHPs default_mimetype ist text/html. Die Prüfung entscheidet
ausschließlich darüber, OB umgeschrieben wird — sie ist keine
Sicherheitskontrolle, und ein „nein" ist immer die harmlosere Antwort.
Runs the IP/user-agent firewall. Hooked on `init` at priority 0 so it fires before the rate limiter, the interceptor and any form-processing handler.
creationell_captcha_run_firewall() : void
Whether comment protection applies to the current request.
creationell_captcha_comments_active() : bool
Injects the widget just above the comment form submit button.
creationell_captcha_comments_render(string $submit_field) : string
The submit button field HTML.
Verifies the captcha before a comment is accepted.
creationell_captcha_comments_verify(array<string, mixed> $commentdata) : array<string, mixed>
Comment data.
Whether login protection is enabled.
creationell_captcha_login_enabled() : bool
Renders the widget inside the login form.
creationell_captcha_login_render() : void
Renders the widget inside `wp_login_form()`-based forms.
creationell_captcha_login_form_middle(string $content) : string
wp_login_form() — used by themes, widgets and shortcodes to embed a
login form outside wp-login.php — never fires the login_form action
above; it runs the login_form_top/login_form_middle/login_form_bottom
filters instead. Without this, such a form posted the same log/pwd/
wp-submit fields as wp-login.php's own form but never got a widget to
solve, so creationell_captcha_login_verify() below rejected even correct
credentials (IN-6).
Existing middle-of-form markup.
Whether the current request is an interactive, browser-submitted login attempt — as opposed to XML-RPC, REST/Application-Passwords, AJAX or a programmatic `wp_signon()` call made by other code.
creationell_captcha_login_is_interactive() : bool
Deliberately does NOT use isset( $_POST['wp-submit'] ) as the signal
(IN-1): wp-login.php's own login handler calls wp_signon()
unconditionally, regardless of whether "wp-submit" was posted, so an
attacker posting directly to wp-login.php could simply omit it and skip
the captcha entirely. The log/pwd field pair is what both
wp-login.php's own form and wp_login_form()-based theme forms actually
send. WooCommerce's My-Account login form uses different field names
(username/password) and is intentionally NOT matched here — it has
its own toggle/verification pair (protect_wc_login, see IN-7).
Verifies the captcha during an interactive login.
creationell_captcha_login_verify(WP_User|WP_Error|null $user, string $username, string $password) : WP_User|WP_Error|null
Authenticated user or error.
Submitted username.
Submitted password.
Whether password-reset protection is enabled.
creationell_captcha_password_reset_enabled() : bool
Renders the widget inside the lost-password form.
creationell_captcha_password_reset_render() : void
Why the current request is not a public lost-password form submission.
creationell_captcha_password_reset_exempt_reason() : string|null
lostpassword_post is NOT a form hook. WordPress core fires it inside
retrieve_password() (wp-includes/user.php), and that function is also the
back end of every administrative and programmatic reset:
wp_ajax_send_password_reset() behind the "Send reset link" button on
user-edit.php, the "Send password reset" bulk action in wp-admin/users.php,
and any WP-CLI or third-party plugin call. None of those ever renders our
widget, so demanding a solved challenge there rejected the request with
"Die Sicherheitsabfrage wurde nicht bestanden" and no mail was ever sent —
retrieve_password() bails out before dispatch once $errors is non-empty.
Returns the reason rather than a bare bool for two reasons: the caller logs
it on the verified event exactly like a bypass reason, and every branch
stays individually observable in the regression test. Under the CLI SAPI a
bool predicate could only ever show its CLI branch — which is why the
administrative branch is checked FIRST here, before the non-interactive
ones. In production the order is immaterial: WP-CLI has neither is_admin()
nor a current user, so it can never take the administrative branch.
Exemption reason, or null for a genuine form submission.
Human-readable label for an exemption reason, for the event log.
creationell_captcha_password_reset_exempt_label(string $reason) : string
Reason key from creationell_captcha_password_reset_exempt_reason().
Verifies the captcha during a password-reset request.
creationell_captcha_password_reset_verify(WP_Error $errors) : void
Password-reset errors (passed by WordPress >= 5.4).
Whether registration protection is enabled.
creationell_captcha_registration_enabled() : bool
Renders the widget inside the registration form.
creationell_captcha_registration_render() : void
Verifies the captcha during registration.
creationell_captcha_registration_verify(WP_Error $errors, string $sanitized_user_login, string $user_email) : WP_Error
Registration errors.
Submitted user login.
Submitted user email.
Ermittelt, WELCHE Adressfamilie ein gespeicherter Listeneintrag vollständig abdeckt und die Liste für diese Familie damit als Auswahl aufhebt.
creationell_captcha_hardening_covered_family(string $entry) : string
Zwei Fragen, zwei vorhandene Wurzeln — hier entsteht bewusst keine dritte IP-Prüfung (die Doppelung eines Validators war in diesem Audit schon zweimal ein Befund, AF-5 und CLI-1):
creationell_captcha_is_valid_ip_or_cidr() — was die Eingabeprüfung
dieser Version akzeptiert, ist per Definition kein Altlastwert. Das hält
Falschmeldungen auf regulären Einträgen von vornherein fern, auch auf
0.0.0.0/1 + 128.0.0.0/1, mit denen ein Betreiber den gesamten
Adressraum weiterhin abdecken darf — wenn er es ausspricht.creationell_captcha_ip_in_cidr() — derselbe Matcher, den Firewall,
Proxy-Vertrauen und Watch-Liste zur Laufzeit benutzen. Trifft er die
erste UND die letzte Adresse einer Familie, deckt der Eintrag alles
dazwischen ebenfalls ab (CIDR-Präfixe sind zusammenhängend).Deshalb erkennt die Prüfung auch Schreibweisen, die ein Textvergleich gegen
„0.0.0.0/0" verfehlt hätte — 10.0.0.0/0 und 0.0.0.0/00 gehören dazu.
Zurückgegeben wird die Familie und nicht nur ein Ja/Nein, weil die Meldung sie
braucht: 0.0.0.0/0 deckt AUSSCHLIESSLICH IPv4 ab, ::/0 ausschließlich
IPv6. creationell_captcha_ip_in_cidr() verlangt gleiche Binärlänge von
Adresse und Netz, ein Eintrag kann also nie beide Familien treffen — und was
er nicht trifft, läuft an der Liste vorbei. Bei den vertrauenswürdigen
Proxies ist genau das der gefährlichere Teil des Fundes (siehe die Meldung
unten), deshalb darf die Familie hier nicht verlorengehen.
Eintrag aus der gespeicherten Liste (bereits getrimmt).
'IPv4', 'IPv6' oder '' — Letzteres heißt: deckt keine Familie vollständig ab, also kein Fund.
Erkennt einen CIDR-Bereich, der in IPv4-mapped Schreibweise notiert ist (`::ffff:0:0/96`, `::ffff:203.0.113.0/120`), und liefert seine gewöhnliche IPv4-Schreibweise zurück.
creationell_captcha_hardening_mapped_ipv4_cidr(string $entry) : string
Die Prüfung eine Etage höher (creationell_captcha_hardening_covered_family())
kann diese Klasse konstruktionsbedingt nicht finden. Sie steigt bei jedem
Eintrag aus, den creationell_captcha_is_valid_ip_or_cidr() annimmt — und
::ffff:0:0/96 ist eine syntaktisch einwandfreie IPv6-Notation mit
Präfixlänge 96, wird also angenommen. Auch die beiden Familien-Sonden greifen
nicht: 0.0.0.0 ist vier Byte lang, der Eintrag sechzehn, und
creationell_captcha_ip_in_cidr() verlangt zu Recht gleiche Binärlänge.
Bis 1.0.2 hat niemand die Client-Adresse vereinheitlicht. Auf einem Server,
dessen PHP die Gegenstelle in mapped Schreibweise sieht (nginx mit
ipv6only=off, Apache auf einem IPv6-Socket, viele Container-Setups), kam
REMOTE_ADDR dort als ::ffff:203.0.113.7 an — sechzehn Byte, dieselbe
Familie wie der Eintrag. ::ffff:0:0/96 traf damit JEDE IPv4-Adresse. In
firewall_trusted_proxies hiess das: jede Gegenstelle gilt als eigener
Proxy, der weitergeleitete Header wird ungeprüft übernommen. Die /0-Regel
der Eingabeprüfung fängt diese Schreibweise nicht.
Seit Modul 27 (Befund I2) kanonisiert creationell_captcha_normalize_ip()
die Client-Adresse auf die gewöhnliche IPv4-Schreibweise — vier Byte. Der
RANGE bleibt bewusst stehen wie geschrieben, weil ein automatisch
umgeschriebenes Vertrauens-Netz die eine Richtung wäre, die hier nie
passieren darf. Folge: der Eintrag trifft heute NICHTS mehr. Gemessen:
ip_in_list( '203.0.113.7', [ '::ffff:0:0/96' ] ) → false
ip_in_list( '::ffff:203.0.113.7', [ '::ffff:0:0/96' ] ) → false
Er ist damit kein offenes Tor mehr, sondern eine Zeile, die für den Betreiber
wie eine wirksame Regel aussieht und keine ist — genau die Klasse unwirksam,
für die dieses Feld existiert. Bei einer Blockliste ist das ein still
verlorener Schutz, bei den Erlaubnis-/Vertrauenslisten eine still verlorene
Ausnahme. Beides gehört gemeldet.
Blanke Einzeladressen in mapped Schreibweise sind NICHT betroffen:
creationell_captcha_ip_in_list() kanonisiert dort beide Seiten, ein
gespeichertes ::ffff:203.0.113.7 trifft also weiterhin. Deshalb verlangt
diese Funktion einen Schrägstrich.
Präfixe unter 96 gelten nicht als mapped Bereich: dort tragen die zwölf
Bytes des Mapped-Präfixes die Entscheidung nicht mehr vollständig, der
Eintrag ist dann ein gewöhnlicher IPv6-Bereich (und ::ffff:0:0/0 fängt
bereits die Familien-Prüfung oben ab).
Eintrag aus der gespeicherten Liste (bereits getrimmt).
Gewöhnliche IPv4-Schreibweise des Bereichs, oder '' wenn der Eintrag kein IPv4-Bereich in mapped Notation ist.
Prüft, ob ein `bypass_ua_allow`-Muster jeden Browser-Kennzeichner trifft.
creationell_captcha_hardening_is_catch_all_pattern(string $pattern) : bool
Entschieden wird das vom Produktions-Matcher selbst, nicht von einer zweiten
Regel: creationell_captcha_wildcard_match() kennt den Schalter
$allow_catch_all, und die Bypass-Auswertung ruft ihn mit false auf
(BK-9). Ein Muster, das MIT erlaubtem Catch-all trifft und OHNE nicht mehr,
ist genau eines, das der Schalter aussortiert — was immer die Regel dahinter
gerade ist. Ein Muster wie *pingdom* trifft in beiden Aufrufen gleich und
fällt damit nicht auf.
Ein Catch-all trifft jeden nicht-leeren Kennzeichner, deshalb genügt eine Probe.
Das Kriterium findet definitionsgemäss nur die Menge, die der Laufzeit-Guard
bereits aussortiert — also Muster mit wirkung: unwirksam. Ein Muster, das
jeden realen Kennzeichner trifft, ohne ein blosses Sternchen zu sein, kommt
durch beide Aufrufe gleich zurück und fällt hier NICHT auf. Das genannte
Gegenbeispiel *pingdom* ist harmlos; Stern-Schraegstrich-Stern ist es
nicht — dieses Muster trifft jeden
User-Agent mit einem Schrägstrich, also praktisch jeden. Diese Klasse deckt
creationell_captcha_hardening_matches_every_user_agent() ab.
Nebenwirkung: bei aktivem CREATIONELL_CAPTCHA_DEBUG schreibt der Matcher
eine Zeile ins Log, wenn er ein Catch-all aussortiert — durch diese Prüfung
also auch dann, wenn gerade keine echte Anfrage ausgewertet wird. Die Aussage
der Zeile stimmt trotzdem: das Muster ist wirkungslos.
Muster aus der gespeicherten Liste.
Prüft, ob ein `bypass_ua_allow`-Muster jeden realistischen User-Agent trifft, ohne vom Catch-all-Guard aussortiert zu werden (Befund B-M2).
creationell_captcha_hardening_matches_every_user_agent(string $pattern) : bool
Der Guard in creationell_captcha_wildcard_match() verwirft ein Muster nur,
wenn nach trim( $pattern, '*' ) nichts übrig bleibt — * und ** also,
Stern-Schraegstrich-Stern nicht. Und dieses Muster trifft jeden
User-Agent, der einen Schrägstrich
enthält: Mozilla/5.0 …, curl/8.5.0, python-requests/2.31.0. Wer diese
Zeile einträgt, hat den gesamten Schutz für jeden Besucher abgeschaltet und
bekommt heute keine Meldung.
Gemeldet wird nur, was JEDE der Sonden trifft — also ein Muster, das keinen
realistischen Kennzeichner mehr auslässt. Ein Existenzquantor hätte
Mozilla* (trifft Browser, nicht curl) und *Chrome* mitgemeldet; das
sind enge Auswahlen, keine Totalabschaltungen, und eine Meldung darüber wäre
genau die Falschmeldung, die den Betreiber zum Wegklicken erzieht. Die
Sondenmenge deckt deshalb absichtlich verschiedene Bauformen ab: Browser mit
Klammerausdruck, blankes Werkzeug ohne Leerzeichen, Bibliothek, Bot mit URL.
Anders als beim blossen Sternchen greift hier KEIN Laufzeit-Guard: das Muster
wird ausgewertet und trifft. Der Fund ist deshalb aktiv, nicht unwirksam.
Muster aus der gespeicherten Liste.
Durchsucht einen Satz Einstellungen nach gefährlicher Bestandskonfiguration.
creationell_captcha_hardening_scan(array<string, mixed> $settings[, bool|null $gd_available = null ]) : array<int, array{id: string, liste: string, eintrag: string, wirkung: string, tab: string, meldung: string}>
Keine Option wird gelesen, keine geschrieben. Der Aufrufer liefert die
Einstellungen; das macht die Erkennung ohne WordPress prüfbar
(tests/test-hardening-migration.php). Der einzige Blick nach draußen ist
extension_loaded( 'gd' ) — und auch der ist über $gd_available
abschaltbar, damit der Test nicht davon abhängt, wie der Rechner gebaut ist,
auf dem er läuft.
Jeder Fund trägt ein Feld wirkung. Drei Werte, weil „greift nicht" drei
verschiedene Dinge heißen kann und der Betreiber sie auseinanderhalten muss:
aktiv — der Eintrag greift zur Laufzeit hier und jetzt.unwirksam — ein Laufzeit-Guard überspringt ihn seit dieser Version
dauerhaft (BK-9, BK-14). Kein Schalter holt ihn zurück.
Er steht trotzdem in der Konfiguration und liest sich für
den Betreiber wie eine wirksame Ausnahme — deshalb genannt.ruhend — der Eintrag greift nur nicht, weil die Funktion, die seine
Liste liest, gerade ausgeschaltet ist. Er ist scharf, sobald
sie eingeschaltet wird — eine Tretmine, kein toter Buchstabe.Warum das Feld nicht pauschal aktiv sein darf: zwei der drei IP-Listen
werden zur Laufzeit hinter einem Schalter gelesen (siehe unten). Auf einer
Installation mit den Vorgabewerten (firewall_behind_proxy = false,
code_challenge_enabled = false) bekäme der Betreiber sonst eine rote
Fehlermeldung über zwei Optionswerte, die kein Codepfad überhaupt anschaut —
und lernt daraus, Meldungen dieses Plugins wegzuklicken.
Gespeicherte Plugin-Einstellungen.
PHP-GD vorhanden? null = selbst
nachsehen. Nur zum Testen gesetzt —
GD ist Teil des Watch-Listen-Gates
(siehe creationell_captcha_should_issue_code_challenge()),
aber eine Umgebungs- und keine
Einstellungsfrage.
Führt den Scan gegen die aktuell gespeicherten Einstellungen aus.
creationell_captcha_hardening_findings() : array<int, array{id: string, liste: string, eintrag: string, wirkung: string, tab: string, meldung: string}>
Kurzkennung des aktuellen Fundbildes.
creationell_captcha_hardening_fingerprint(array<int, array<string, string>> $findings) : string
Ein: id und wirkung. Nicht der Meldungstext — sonst ließe eine
Umformulierung beim nächsten Update einen bereits weggeklickten Hinweis wieder
auftauchen. wirkung muss dagegen einfließen: ein ruhend-Fund wird zu einem
aktiv-Fund, sobald der Betreiber die zugehörige Funktion einschaltet, ohne
dass sich die id ändert. Ohne dieses Feld bliebe die ausgeblendete Warnung
auch dann ausgeblendet, wenn aus der Tretmine ein Loch geworden ist.
Umgekehrt bringt ein NEUER oder geänderter Fund den Hinweis zurück, obwohl der Benutzer den vorigen weggeklickt hat — genau das ist gewollt: ausgeblendet wird ein bestimmtes Fundbild, nicht die Meldung als solche.
Die id geht gehasht ein, nicht roh: sie enthält gespeicherte Listeneinträge
und darf im Zusammenbau kein Trennzeichen zerschießen (ein bypass_cookies-
Eintrag darf jedes Zeichen enthalten, auch Tabulator und Zeilenumbruch).
Fundliste aus dem Scan.
16 Hex-Zeichen, oder '' wenn es nichts zu melden gibt.
Baut das Ziel des Redirects nach dem Ausblenden-Klick: dieselbe Seite ohne den Ausblenden-Parameter und ohne die Nonce.
creationell_captcha_hardening_dismiss_redirect_target() : string
Wirkung des ursprünglichen Fundes war gering und ist es geblieben: Der
Handler hängt hinter current_user_can( 'manage_options' ) und
check_admin_referer(), ein Administrator müsste also selbst eine
//-Schreibweise einer wp-admin-URL aufrufen, und wp_safe_redirect()
fängt einen fremden Host ohnehin ab. Der Ausgang war der von N1 an der
Under-Attack-Stelle beschriebene: statt zurück auf die Seite, von der er
kam, landete der Administrator auf der wp-admin-Startseite — und der
Hinweis, den er gerade weggeklickt hat, wäre dort erneut zu sehen.
Relativer Ziel-URI für wp_safe_redirect().
Nimmt den Ausblenden-Klick entgegen.
creationell_captcha_hardening_handle_dismiss() : void
Gespeichert wird die serverseitig NEU BERECHNETE Kennung, nicht die aus der URL. Sonst ließe sich mit einem präparierten Link ein Fundbild ausblenden, das es noch gar nicht gibt — der Hinweis wäre schon weggeklickt, bevor er das erste Mal fällig wird.
Zeigt die gefundene Bestandskonfiguration als Admin-Hinweis.
creationell_captcha_hardening_render_notice() : void
Auf allen Admin-Seiten, nicht nur auf den Plugin-Seiten: es geht um einen Schutz, der gerade nicht greift, und wer das erfahren soll, muss dafür nicht erst zufällig die Einstellungsseite öffnen. Damit der Hinweis trotzdem keine Tapete wird, ist er pro Benutzer ausblendbar (und kommt bei einem neuen Fund von selbst zurück, siehe creationell_captcha_hardening_fingerprint()).
Nur für Benutzer mit manage_options — niemand sonst könnte etwas ändern,
und die Meldung nennt Details der Firewall-Konfiguration.
Resolves the active WordPress locale to a vendored ALTCHA locale code.
creationell_captcha_resolve_widget_locale() : string|null
Returns the locale string (e.g. "de", "fr-fr", "pt-br") if a mapping exists, or null when the WP locale is not in the vendor set — in which case the widget renderer skips both the language attribute and the i18n script enqueue, letting the widget fall through to its own detection (which has only the EN built-in available).
Two filters are applied: creationell_captcha_widget_locale_map to
extend / override the lookup table, and
creationell_captcha_widget_locale for last-mile overrides after
lookup.
Vendored ALTCHA locale code or null.
Default plugin settings.
creationell_captcha_get_default_settings() : array<string, mixed>
Current plugin settings, merged over the defaults.
creationell_captcha_get_settings([bool $force_refresh = false ]) : array<string, mixed>
Memoised for the duration of the request — get_option() itself is cheap thanks to WP's object cache, but the defaults-merge over ~70 keys adds up across the 10+ call sites per request (Interceptor, Firewall, Rate- Limiter, Under-Attack, every form integration). The cache is invalidated automatically when the option is added, updated or deleted.
Re-read from the DB even if a cached copy exists. Used by the invalidation hook.
Drops the in-request settings cache. Wired to the option-change hooks below so callers that read settings after an update see the fresh value.
creationell_captcha_invalidate_settings_cache() : void
Whether the captcha is globally disabled via the wp-config constant.
creationell_captcha_is_disabled() : bool
Whether ext-sodium (required for Argon2id) is available.
creationell_captcha_sodium_available() : bool
Generates both HMAC secrets and persists them (non-autoloaded).
creationell_captcha_generate_secrets() : array{signature: string, key_signature: string}
Returns a stored HMAC secret, generating + persisting it on first use.
creationell_captcha_get_secret(string $which) : string
Either 'signature' or 'key_signature'.
The HMAC signature secret (signs each challenge).
creationell_captcha_get_hmac_secret() : string
A wp-config constant takes precedence over the stored option.
The HMAC key-signature secret (enables the fast verification path).
creationell_captcha_get_hmac_key_secret() : string
A wp-config constant takes precedence over the stored option.
Derives a purpose-bound HMAC key from the plugin's base signature secret.
creationell_captcha_derive_hmac_key(string $purpose) : string
Wurzel 3.5: the challenge signature, the under-attack pass cookie and the
ctx suppression token were all MACs under the SAME key. Cross-purpose use
was only prevented by the differing message layout — a fragile property that
breaks silently as soon as one token family changes its format. HKDF-style
expansion with a purpose label makes the separation structural: a MAC minted
for one purpose verifies under no other key.
The base secret stays exactly where it is (CREATIONELL_CAPTCHA_HMAC_SECRET
/ the stored option) — the derivation sits in front of it, so the ALTCHA
library keeps signing challenges with the unchanged base key.
Stable purpose label, e.g. underattack-pass.
64-char hex key, or '' when no base secret is available (callers must fail closed on '').
The request's User-Agent, capped at 256 bytes. MAC input for the under-attack tokens — never rendered, never stored.
creationell_captcha_client_ua() : string
The fingerprint the under-attack pass cookie is bound to (BK-3).
creationell_captcha_underattack_pass_binding() : string
Two components, neither of which travels inside the cookie. Only one of them is load-bearing, and it is worth naming which: the NETWORK is what a recipient cannot bring along, because it is where his packets come from. The User-Agent he can bring along — he sends it himself, and whoever passes the cookie on passes the UA string on with it. So the UA is a cheap extra, the network is the half that actually refuses a transferred cookie.
creationell_captcha_get_client_ip()
so a forwarded header only counts behind a trusted proxy (BK-7/BK-8) and
never straight off an attacker-set header;Trade-off, deliberately made here and not at the two call sites: the User-Agent alone is weak (whoever passes the cookie on passes the UA string on with it), the full address breaks on every mobile hand-over. The network is the middle ground — it survives the address churn inside one access network (CGNAT pool, IPv6 privacy extensions) and still refuses a cookie that travelled to a different one. A false negative costs exactly one extra proof-of-work: the visitor sees the interstitial again, solves it and gets a cookie bound to the new network. No lock-out, no loop.
Binding string; '' disables the binding entirely.
Mints an under-attack pass token for the current visitor.
creationell_captcha_underattack_pass_issue(int $expiry) : string
Absolute Unix timestamp the pass runs out at.
<expiry>.<mac> token, or '' when no key could be derived.
Verifies an under-attack pass token against the CURRENT visitor.
creationell_captcha_underattack_pass_check(string $cookie) : bool
BK-3: before 1.1.0 the MAC covered the expiry timestamp and nothing else, so
one solved interstitial produced a bearer token that worked from any client,
any address, for up to underattack_pass_duration (86400 s). The MAC now
covers the visitor binding as well, which is not part of the cookie value.
Raw cookie value.
Mints a single-use `ctx` suppression token for one interstitial rendering.
creationell_captcha_underattack_ctx_issue() : string
BK-4/CM-4/W5: the old token was HMAC(secret, 'ua-ctx|' . floor(time()/300))
— identical for every visitor inside a five-minute bucket and accepted for
the current AND the previous bucket, i.e. up to ten minutes. It was handed
to every anonymous 503 visitor inside the HTML. Unforgeable, yes — but
trivially obtainable, transferable and replayable.
The replacement carries a random nonce (so every rendering gets its own
token), an explicit expiry and a MAC over nonce, expiry and the visitor's
User-Agent. It is burned on first use in …_ctx_check().
What this closes, precisely — and what it does NOT:
Engine::verify() — outside this file (R1 in the task report).WHAT SINGLE USE COSTS (m7): the token is spent by the FIRST challenge fetch
of the interstitial that carried it. If the very same 503 HTML reaches a
browser a second time — restored from the back/forward cache, or replayed by
a full-page cache or CDN that ignores the response headers — the widget
fetches again with a token that is already burned. /challenge then answers
without the suppression, and with code_challenge_enabled on that means the
INVISIBLE interstitial widget receives an image code it cannot display: that
visitor never passes the gate at all. The interstitial itself asks not to be
stored (nocache_headers() sends no-store, private, measured against the
WordPress 7.0.2 of the test instance), so this needs a cache that disregards
that — but such caches exist, and "under attack" is exactly when an operator
puts one in front of the site. Making that visible is a doctor job
("under-attack mode on and a full-page cache detected"); it cannot be fixed
from the token side without giving up the single use that closed BK-4.
Why no client-network binding here, unlike the pass cookie: it would not
reduce what an attacker can do — every bot can fetch its own 503 page, so
handing a token around buys nothing over simply acquiring one — while a
false negative here is a dead end instead of a retry. A rejected pass cookie
costs one extra proof-of-work; a rejected ctx gets the interstitial's
invisible widget an image code it cannot display, and that visitor never
passes the gate at all. On top of that the address is not even constant
across the two requests of ONE visitor: the XHR that fetches the challenge
can leave through a different address than the page view that carried the
token (dual-stack clients pick the family per connection, egress pools
rotate well beyond a /24).
<nonce>.<expiry>.<mac>, or '' when no key could be derived.
Verifies a `ctx` suppression token and consumes it.
creationell_captcha_underattack_ctx_check(string $token) : bool
Returns true at most ONCE per token: the nonce is recorded for the rest of the token's lifetime, every later presentation of the same token fails.
Raw ctx query value.
Shared captcha engine instance.
creationell_captcha_engine() : Engine
Write a message to the debug log when CREATIONELL_CAPTCHA_DEBUG is active.
creationell_captcha_log(string $message) : void
W1-13: Steuerzeichen werden entfernt, BEVOR die Zeile geschrieben wird —
dieselbe Reduktion, die Analytics::current_path() für die Log-Tabelle
vornimmt. Mehrere Meldungen tragen vom Absender bestimmte Bestandteile in
die Zeile, allen voran der Interceptor („interceptor blocked POST to " plus
dem EINMAL dekodierten Pfad, class-interceptor.php): ein anonymer
POST /kontakt%0A auf einen per /kontakt* geschützten Pfad wird von
WordPress geroutet (WP::parse_request() trimmt, und die Rewrite-Regel
^kontakt/?$ trifft dank $ vor dem Zeilenumbruch), der Interceptor
blockiert — und die Logzeile enthielt einen echten Zeilenumbruch. Damit
bestimmte der Absender, wo eine Zeile im PHP-Fehlerlog endet, und konnte
eine zweite, frei gewählte anhängen.
Bewusst hier und nicht an der einen Aufrufstelle: dies ist die Senke, durch die JEDE Meldung des Plugins geht, und keine von ihnen enthält eine beabsichtigte mehrzeilige Ausgabe (nachgezählt über alle Aufrufer). Eine Reparatur je Aufrufstelle wäre dieselbe Zeile mehrfach — und die nächste neue Meldung hätte sie wieder nicht.
Message to log.
Canonicalises an IPv4-mapped IPv6 address (`::ffff:a.b.c.d`) into its plain IPv4 spelling. Every other value — including anything that is not an IP at all — is handed back unchanged.
creationell_captcha_normalize_ip(string $ip) : string
On every server whose PHP sees the peer address in IPv4-mapped notation
(nginx listen [::]:443 ipv6only=off, Apache on an IPv6 socket, plenty of
container and proxy setups) REMOTE_ADDR reads ::ffff:203.0.113.7 for an
ordinary IPv4 client. That string passes FILTER_VALIDATE_IP — it is a
perfectly valid IPv6 address — so nothing ever complained, but:
ip_in_cidr() refuses a family change (4 vs. 16 bytes, correctly so),
and ip_in_list() otherwise compares plain strings;firewall_ip_block, firewall_ip_allow, firewall_trusted_proxies
and code_challenge_watchlist matched NOTHING for those clients — the
IP firewall was silently inert, and the admin who allow-listed his own
address locked himself out anyway;anonymize_ip() reduced all of them to ::, which took the network
half out of the BK-3 under-attack pass binding and left a pure bearer
token behind.The check runs on the binary form rather than on the string, so the rarer
spellings (::FFFF:cb00:7107, 0:0:0:0:0:ffff:203.0.113.7) are caught as
well. :: and ::ffff:0:0:0 are NOT mapped addresses and stay untouched.
Candidate address.
Plain IPv4 spelling for mapped addresses, else the input.
Resolves the client IP address.
creationell_captcha_get_client_ip() : string
Returns the validated REMOTE_ADDR by default. When the firewall_behind_proxy
setting is on, the configured forwarded header is used instead — falling back
to REMOTE_ADDR if it yields no valid IP.
This is the single ingress for client addresses: every list check, the rate-limit bucket key, the pass binding and the event log take their value from here. Canonicalising IPv4-mapped addresses therefore happens HERE and not at each of those places (I2).
WHAT THE FALLBACK COSTS (m3 / W2-3) — say it here, because it is not obvious
at the call sites: every return $remote below hands the PROXY's address to
everything downstream. That is the safe direction (BK-8: an entry we cannot
classify must never let a forged one to its left win), but it is not a free
one. If a fallback fires on EVERY request — an upstream that appends
unknown or an obfuscated identifier per RFC 7239, an Azure-style
ip:port hop, a firewall_proxy_header naming a header this installation
does not actually receive — then all visitors share one address:
firewall_ip_block and code_challenge_watchlist hit all or nothing;firewall_ip_allow, every
visitor is bypassed.Each fallback therefore names itself through creationell_captcha_log().
That is only visible with CREATIONELL_CAPTCHA_DEBUG; making it visible
without the debug switch belongs to the settings help text and to
wp creacaptcha doctor, not here.
Whether an IP matches any entry in a list of IPs or CIDR ranges.
creationell_captcha_ip_in_list(string $ip, mixed $list) : bool
This is the choke point all four address lists run through — blocklist, allowlist, trusted proxies and the code-challenge watchlist — which is why the IPv4-mapped canonicalisation is applied here and not four times over.
Admin-typed entries are canonicalised as well, so an installation that spelled
an entry ::ffff:203.0.113.7 (the only spelling that worked on an affected
server before this release) keeps matching. CIDR entries are deliberately NOT
rewritten: a mapped range like ::ffff:0:0/96 would widen to "every IPv4
address", and silently widening a TRUSTED-PROXY range is the one direction
this plugin must never take (LK-13/AF-5). A CIDR written in mapped notation
therefore stops matching — see the note on ip_in_cidr().
The client IP.
A list of IPs / CIDR ranges (non-arrays are ignored).
Whether an IP falls within a CIDR range. Supports IPv4 and IPv6.
creationell_captcha_ip_in_cidr(string $ip, string $cidr) : bool
The subject is canonicalised (I2); the RANGE is taken as written. A range
spelled in IPv4-mapped notation (::ffff:203.0.113.0/120) consequently no
longer matches an IPv4 client — deliberately, because converting it would
mean rewriting prefix lengths, and a wrong prefix in a trusted-proxy list is
the failure mode this plugin has already had to close twice.
The client IP.
A CIDR range, e.g. "203.0.113.0/24".
Whether a string is a valid IP address or CIDR range (IPv4 or IPv6).
creationell_captcha_is_valid_ip_or_cidr(string $entry) : bool
Prefix length 0 (0.0.0.0/0, ::/0) is refused: it is valid CIDR notation
but matches every address, so as a firewall-allow, trusted-proxy or
blocklist entry it silently disables the very list it is in (AF-5). An admin
who really wants to cover the whole address space can still spell it out as
two halves (0.0.0.0/1 + 128.0.0.0/1) and thereby say so on purpose.
This is an input guard only — it decides what may be STORED. Values already in the database keep working; reporting on those is the fail-safe migration's job, not this function's.
The candidate string.
Whether a subject matches any of the given wildcard patterns (case-insensitive).
creationell_captcha_wildcard_match(string $subject, mixed $patterns[, bool $empty_subject_matches = false ][, bool $allow_catch_all = true ]) : bool
The pattern alphabet is the same as the firewall UA-blocklist: * is the
single wildcard, everything else is matched literally.
The two edge cases used to be decided implicitly, and the decision was wrong for one of the two call sites (BK-9). Both are now the caller's to make:
$empty_subject_matches — a missing header is not "the empty string", it
is no information at all. For a blocklist "no match" is the safe answer,
for an allowlist it is too, but the two reach it for opposite reasons, so
neither may inherit it silently.$allow_catch_all — a pattern of nothing but * matches every non-empty
subject. Harmless in a blocklist, a total shutdown of the protection in
an allowlist. Pass false there and such a pattern is skipped.$allow_catch_all STOPS — READ THIS BEFORE TRUSTING ITThe guard is SYNTACTIC and nothing else: it drops a pattern when
trim( $pattern, '*' ) leaves nothing behind, i.e. *, **, ***. It does
NOT drop a pattern that merely happens to match everything in practice.
Measured against the production path: star-slash-star (written out because the
literal form would close this comment block) passes this guard and matches
every realistic User-Agent — every one of them carries a slash. Same for
star-dot-star. false here therefore means "no bare star", not "no
catch-all".
That boundary is deliberate. "Matches every real subject" is not a decidable
property of a pattern; the nearest thing to it is the five-probe criterion in
creationell_captcha_hardening_matches_every_user_agent(), and a heuristic
has no business deciding a single request — least of all one that would run
on every request, for every stored pattern. Applying it here would also
silently reinterpret patterns an operator has already stored. Reporting such
an entry is the fail-safe migration's job
(includes/hardening-migration.php, section 3, finding class jeder-ua,
wirkung: aktiv): it names the entry and leaves the decision with the
operator. tests/test-bypass-roots.php section 4a pins both halves.
The string to test.
A list of patterns; non-arrays return false.
What an empty subject means for this call site. Default false (previous behaviour).
Whether a bare * pattern is honoured.
Default true (previous behaviour).
Returns the canonical list of private/loopback CIDR ranges used when the `firewall_trust_private_ranges` toggle is active.
creationell_captcha_private_ranges() : array<string|int, string>
Reads the optional `CREATIONELL_CAPTCHA_TRUSTED_PROXIES` wp-config constant as a list. Accepts either a string array or a comma/whitespace-separated scalar; invalid entries are dropped.
creationell_captcha_trusted_proxies_constant() : array<string|int, string>
Whether the given IP belongs to a trusted upstream proxy.
creationell_captcha_is_trusted_proxy(string $ip) : bool
Sources are checked in this order; the first match wins:
A validated client IP address.
Pure bypass evaluator — checks the three bypass sources against the supplied inputs without touching $_SERVER, $_COOKIE or any static cache. The caller is responsible for providing the values.
creationell_captcha_evaluate_bypass(string|null $ip, string|null $ua, array<string, string> $cookies) : array{reason: string, source: string}|false
Sources are checked in this order; the first match wins:
Client IP, or null to skip the IP check.
User-Agent, or null to skip the UA check.
Cookie map (name => value).
Whether the current request is allowed to bypass captcha, under-attack and firewall protections. Reads $_SERVER, $_COOKIE and the request's client IP, then delegates to `creationell_captcha_evaluate_bypass()`.
creationell_captcha_request_bypassed() : array{reason: string, source: string}|false
Result is memoised for the request — settings, IP and cookies do not change
within a single PHP request. Only reason flows into the event-log context;
source is exposed for diagnostic logging by callers.
Validates a single interceptor-action pattern.
creationell_captcha_validate_action_pattern(string $entry) : string|null
Allowed: lowercase/uppercase letters, digits, _, -, * (wildcard),
with an optional leading ! for exclusion patterns. Empty input or
patterns of only ! are rejected.
Raw entry (already trimmed by the caller).
Normalised entry, or null if invalid.
Validates a single bypass-cookie entry of the form `name=value`.
creationell_captcha_validate_cookie_entry(string $entry) : string|null
Name must be alphanumeric, _ or -. The value is length-capped to 200
bytes and passed through sanitize_text_field(); an entry whose value is
empty — before or after sanitising — is refused (BK-14): hash_equals('','')
is true, so such an entry would let anybody past who sends the bare cookie
name. A bypass cookie is a shared secret; a secret of zero length is none.
Raw entry (already trimmed by the caller).
Normalised name=value entry, or null if invalid.
Truncates an IP for DSGVO-compliant storage. IPv4 → last octet zeroed, IPv6 → last 80 bits zeroed. Invalid IPs return ''.
creationell_captcha_anonymize_ip(string $ip) : string
I2: an IPv4-mapped address is canonicalised first. Without that every such
client reduced to :: — one value for the whole IPv4 internet, which made
the event log useless AND emptied the network half of the under-attack pass
binding (BK-3). Callers normally pass creationell_captcha_get_client_ip(),
which canonicalises already; this repeats it for the direct callers.
A validated client IP address.
Returns a JSON-encoded fingerprint of $_POST: { field-name: value-byte-length }.
creationell_captcha_request_body_fingerprint() : string
No values are recorded — only structural metadata for attack-pattern
diagnosis. Field names that contain known sensitive substrings (password,
iban, api_key, …) are replaced with [masked:<8-char-sha256>] so the
fingerprint does not leak custom-form schema (e.g. bank_iban_input).
Output is length-capped to 2048 bytes; if longer, the JSON is collapsed
to "}" rather than truncated mid-entry.
Sends a fail-closed block response and terminates the request.
creationell_captcha_block_response(int $status, string $message[, int $retry_after = 0 ]) : void
HTTP status code (403 firewall, 429 rate limit).
The message shown to the client.
Optional Retry-After value in seconds.
Base64URL encoder (RFC 4648 §5) — strips standard-base64 padding and replaces +/ with -_ so the value is URL-safe.
creationell_captcha_base64url_encode(string $bytes) : string
Raw bytes to encode.
Base64URL decoder — accepts unpadded URL-safe input and returns the raw bytes. Returns the empty string on malformed input (no exceptions).
creationell_captcha_base64url_decode(string $encoded) : string
URL-safe base64 string.
Reads the current rate-limit counter for an IP without incrementing it.
creationell_captcha_ratelimit_current_count(string $ip) : int
Uses the same bucket key as Creationell\Captcha\RateLimiter::run() so the
value matches what the run-loop would see. Returns 0 if no transient exists
for the current window.
Client IP (call creationell_captcha_get_client_ip()).
Whether the Contact Form 7 integration is active.
creationell_captcha_cf7_active() : bool
Registers the [creationell_captcha] Contact Form 7 form-tag.
creationell_captcha_cf7_register_tag() : void
Registered unconditionally (no creationell_captcha_cf7_active() guard) so CF7 always recognises the tag and never prints it as raw text; the tag handler returns an empty string when the integration is inactive.
Renders the widget for the [creationell_captcha] form-tag.
creationell_captcha_cf7_tag_handler() : string
Auto-injects the widget into CF7 forms without a [creationell_captcha] tag.
creationell_captcha_cf7_auto_inject(string $elements) : string
The form's inner HTML.
Verifies the captcha on a Contact Form 7 submission.
creationell_captcha_cf7_verify(mixed $spam, mixed $submission) : bool
Hooked on wpcf7_spam: returning true marks the submission as spam, which
CF7 then rejects through its standard flow.
Whether CF7 already classified the submission as spam.
The WPCF7_Submission object.
Whether the Forminator integration is active.
creationell_captcha_forminator_active() : bool
Auto-injects the widget before the submit button of a Forminator custom form.
creationell_captcha_forminator_inject(mixed $html, mixed $form_id) : string
The forminator_render_form_submit_markup filter also fires for polls and
quizzes; injection is restricted to the forminator_forms post type.
The submit-section HTML.
The form's post ID.
Verifies the captcha on a Forminator custom-form submission.
creationell_captcha_forminator_verify(mixed $errors) : array<int, array<string, string>>
Hooked on forminator_custom_form_submit_errors (custom forms only): a
non-empty errors array makes Forminator reject the submission.
The current array of submission errors.
Whether any WooCommerce protection applies right now.
creationell_captcha_woocommerce_active() : bool
Shared gate that the per-form predicates _wc_*_active() route through —
encapsulates the kill-switch, the class_exists check and the master
toggle so each form predicate just needs to add its own sub-toggle check.
Whether the currently configured WooCommerce checkout page uses the Block Checkout (the `woocommerce/checkout` block) rather than the classic `[woocommerce_checkout]` shortcode.
creationell_captcha_wc_uses_block_checkout() : bool
IN-2: this codebase only ever protects the classic checkout — the render
hook (woocommerce_review_order_before_submit) and the verify hook
(woocommerce_after_checkout_validation) both fire exclusively from
WC_Checkout::process_checkout(), which the Block Checkout's Store API
route never calls. This function exists purely to DETECT that gap for
wp creacaptcha doctor and the Settings help text — it deliberately does
NOT attempt to inject the widget or hook the Store API route (out of
scope per the audit's Phase-2 plan; that would be new functionality
belonging to its own module).
Whether the WooCommerce checkout protection is active.
creationell_captcha_wc_checkout_active() : bool
Renders the widget directly before the Place-Order button on the checkout.
creationell_captcha_wc_checkout_render() : void
Per-request cache of the checkout's own verify verdict, keyed by the raw `altcha` payload currently in `$_POST`.
creationell_captcha_wc_checkout_verify_cache([bool|null $set = null ]) : bool|null
IN-3: the classic checkout, when the customer checks "create an
account?", posts exactly ONE altcha payload but runs it through TWO of
our verify hooks within the same request —
creationell_captcha_wc_checkout_verify() first (inside
WC_Checkout::validate_checkout()), then
creationell_captcha_wc_registration_verify() a few lines later (inside
WC_Checkout::process_customer() → wc_create_new_customer()), reading
the IDENTICAL $_POST['altcha']. The underlying ALTCHA challenge is
single-use (class-engine.php's replay-transient marker), so the second
check would always find the payload already marked used and fail — an
account-creating order could never succeed. Recording the checkout
verdict here lets the registration verify reuse it for the same payload
instead of re-running the used-marker check a second time.
Scoped to this file/request only (static, resets every request) — it
does not weaken replay protection across requests: a genuinely replayed
payload from an EARLIER request still hits the used-marker on its very
first check here, because this cache starts empty every time.
Pass a bool to record the verdict for the current raw payload; omit (null, default) to just read a previously recorded one.
The cached verdict for the current raw payload, or
null if none has been recorded yet this request.
Verifies the captcha during checkout validation.
creationell_captcha_wc_checkout_verify(array<string, mixed> $data, mixed $errors) : void
woocommerce_after_checkout_validation fires inside WooCommerce's
process_checkout() after all other validation has run; adding an error
to the passed-through WP_Error aborts the order.
Posted checkout data (unused).
The checkout WP_Error (passed by reference of the object).
Whether the WooCommerce my-account login protection is active.
creationell_captcha_wc_login_active() : bool
Renders the widget at the bottom of the WooCommerce login form.
creationell_captcha_wc_login_render() : void
Verifies the captcha on a WooCommerce my-account login submission.
creationell_captcha_wc_login_verify(mixed $validation_error, string $username, string $password) : mixed
Returns a WP_Error to fail the login; otherwise returns the incoming
$validation_error value unchanged (so other filters can keep working).
The current validation error (WP_Error|null|false).
Submitted username (unused).
Submitted password (unused).
Whether the WooCommerce registration protection is active.
creationell_captcha_wc_registration_active() : bool
Renders the widget at the bottom of the WooCommerce registration form.
creationell_captcha_wc_registration_render() : void
Reads a nonce value the way the given WooCommerce field name(s) would, replicating WooCommerce core's own field-name-agnostic fallback: the dedicated field wins if present, otherwise the generic `_wpnonce` field is used. WooCommerce's own nonce checks do not care WHICH field carried the value — only whether the value itself verifies against the action name.
creationell_captcha_wc_nonce_verifies(array<string, mixed> $source, string $dedicated_field, string $action) : bool
Review fix (Fix-Runde 1, IN-4 critical): an earlier version of this guard
checked the dedicated field's mere PRESENCE
(isset( $_POST['woocommerce-register-nonce'] )) as its "genuine form"
signal. That was exploitable — WooCommerce itself resolves the nonce
VALUE via the same _wpnonce fallback implemented here, so an anonymous
attacker could read the nonce value off the real registration form's
hidden field and resubmit register=…&email=…&_wpnonce=<value> while
OMITTING the woocommerce-register-nonce field. WooCommerce would still
accept the nonce via its fallback and create the account, while the old,
presence-only guard saw no dedicated field and treated the request as
"no form context" — skipping the captcha check entirely. Checking the
NONCE VALUE (via wp_verify_nonce()) instead of field presence closes
this: the fallback is followed identically on both sides, so there is no
field-name an attacker can omit to fool this guard while WooCommerce
itself still accepts the request.
$_POST or $_REQUEST.
The form's own nonce field name.
The nonce action to verify against.
Whether the current `wc_create_new_customer()` call originates from a genuine WooCommerce registration- or checkout-form POST, as opposed to a Store-API, programmatic or CLI call that never had a form (and therefore no `altcha` field) in the first place.
creationell_captcha_wc_registration_form_context() : bool
IN-4: wc_create_new_customer() fires woocommerce_registration_errors
— the filter this module hooks — UNCONDITIONALLY, including for the
Woo Blocks Store API's own account-creation path and for direct
programmatic calls (wp eval, custom scripts, other plugins). Hard-
rejecting those whenever no altcha field happens to be present made
wc_create_new_customer() unusable outside an actual captcha-protected
form — the customer was simply never created.
Both genuine entry points already verify their OWN nonce, inside
WooCommerce core, before ever calling wc_create_new_customer() — so
independently re-verifying the SAME nonce value/action here (not
consuming anything, wp_verify_nonce() is read-only) is a safe, precise
signal, as long as it replicates WC's exact field-name fallback (see
creationell_captcha_wc_nonce_verifies() above — a presence-only check
was exploitable, see that function's docblock):
WC_Form_Handler::process_registration() gates on
isset( $_POST['register'], $_POST['email'] ) plus a valid
woocommerce-register nonce (dedicated field
woocommerce-register-nonce, falling back to _wpnonce) before ever
calling wc_create_new_customer().WC_Checkout::process_checkout()
verifies the woocommerce-process_checkout nonce (dedicated field
woocommerce-process-checkout-nonce, falling back to _wpnonce, read
from $_REQUEST) before validate_checkout()/process_customer()
run — and by the time this filter fires from inside
process_customer(), our OWN checkout verify
(creationell_captcha_wc_checkout_verify()) has already run too, see
IN-3's cache below.CLI/cron/XML-RPC calls need no explicit exclusion here: none of them ever
populate $_POST/$_REQUEST with a nonce that verifies against either
action, so both checks below already resolve to false for them without
a dedicated SAPI/constant check — one less thing to keep in sync, and it
keeps this guard testable by directly setting $_POST (see tests/eval/).
Deliberately does NOT exclude on wp_doing_ajax(): WooCommerce's classic
checkout is itself normally submitted via wc-ajax=checkout
(WC_AJAX::checkout(), which defines DOING_AJAX itself before
dispatch) — excluding AJAX outright would silently re-open IN-3/IN-4 for
the majority of real-world checkouts.
Verifies the captcha during WooCommerce my-account registration.
creationell_captcha_wc_registration_verify(mixed $errors, string $username, string $email) : mixed
The current WP_Error carrier from WooCommerce.
Submitted username (unused).
Submitted email (unused).
Whether the WooCommerce lost-password render is active.
creationell_captcha_wc_lost_password_active() : bool
Renders the widget inside the WooCommerce lost-password form.
creationell_captcha_wc_lost_password_render() : void
Whether the WPForms integration is active.
creationell_captcha_wpforms_active() : bool
Auto-injects the widget directly before the WPForms submit button.
creationell_captcha_wpforms_inject(array<string, mixed> $form_data[, mixed $form = null ]) : void
Fires inside the
WPForms form configuration.
WPForms form post (unused, optional — der
Hook wpforms_display_submit_before liefert nur $form_data).
Verifies the captcha on a WPForms submission.
creationell_captcha_wpforms_verify(array<int, mixed> $fields, array<string, mixed> $entry, array<string, mixed> $form_data) : void
Hooked on wpforms_process (action). On failure we set an entry in
wpforms()->process->errors[ $form_id ]['header'] — WPForms then renders
the message above the form and refuses to save the entry.
Sanitized field values (unused).
Raw $_POST['wpforms'] (unused).
Form configuration.
Conditionally starts the output buffer on template_redirect priority 0.
creationell_captcha_interceptor_inject_buffer_start() : void
The buffer only runs when (a) the master interceptor toggle is on, (b) at least one inject path is configured, AND (c) the current request path matches that pattern list. On non-matching pages the request is unaffected.
Buffer callback. Replaces every `<form …>…</form>` with the same form plus an `<altcha-widget>` inserted directly before `</form>`. Idempotent — forms that already contain `<altcha-widget` are returned unchanged.
creationell_captcha_interceptor_inject_buffer(string $html) : string
Full page HTML.
Runs the request interceptor. Hooked on `init` at priority 1 so it fires before any form-processing handler.
creationell_captcha_run_interceptor() : void
Registers one or more path patterns to be guarded by the interceptor.
creationell_captcha_protect_path(string|array<int, string> $patterns) : void
Developer API — later form-plugin integrations call this to protect their
submission endpoints without an admin entering patterns by hand. The
patterns are merged into the creationell_captcha_interceptor_paths filter.
A path pattern or list of patterns.
Runs on plugin activation: seeds default options and HMAC secrets.
creationell_captcha_activate() : void
Runs on plugin deactivation: clears every cron slot the plugin owns, sweeps the expired replay markers out of the options table and lets every module react via the `creationell_captcha_deactivated` action hook.
creationell_captcha_deactivate([bool $network_deactivating = false ]) : void
B-M5: cron slots and replay markers are per-site data, so on a network-wide
deactivation the work has to be done on every site of the network — not just
on whichever site happens to be current when deactivate_{$plugin} fires
(that is the network admin's site, usually the main site). uninstall.php
already walks the network this way since DS-3; this is the same walk for the
same reason.
Whether the plugin is being deactivated for
the whole network. WordPress passes this as
the single argument of deactivate_{$plugin}
(wp-admin/includes/plugin.php,
deactivate_plugins()); the default keeps
the function callable by hand.
The deactivation work for exactly one site: clears the three cron slots this plugin owns there and sweeps that site's expired replay markers.
creationell_captcha_deactivate_site() : void
Everything in here is relative to the CURRENT site, so it is safe to call
from inside a switch_to_blog() bracket — wp_clear_scheduled_hook() works
on the current site's cron option and the sweep re-reads $wpdb->options
on every call.
Runs the per-site deactivation work on every site of the network.
creationell_captcha_deactivate_network() : int
Walks the sites in batches instead of materialising every site id at once —
same shape and same batch size as the DS-3 walk in uninstall.php, so both
routines behave the same way on the same network.
Number of sites processed.
Deletes those replay-marker options of the current site whose lifetime has already run out.
creationell_captcha_delete_expired_replay_markers() : int
Live markers are deliberately left in place. Removing them as well would be
tidier — nothing sweeps them while the plugin is unloaded — but it would
re-open the window CM-9 closed: a payload whose marker is gone passes
Engine::verify() a second time as long as its own signed expiresAt has
not been reached (challenge_expiry, default 300 s, settings range
60–3600 s, checked inside the ALTCHA library independently of the marker —
lib/altcha-org/altcha/src/Altcha.php, verifySolution()). Deactivating
and re-activating inside that window is an everyday admin action — update,
debugging, changing the plugin load order — so the marker has to survive it.
What stays behind when the plugin is never re-activated is one option row per
payload whose challenge has not run out yet (since 1.1.0 the marker lifetime
is the challenge's own remaining validity — Engine::replay_marker_lifetime()).
uninstall.php removes those unconditionally, and after a re-activation the
next successful claim re-arms the cron slot cleared in
creationell_captcha_deactivate_site() (Engine::claim_replay_marker() →
schedule_replay_cleanup()), whose sweep then drops them.
The predicate is the one Engine::cleanup_replay_markers() uses: the marker
value is the Unix timestamp the marker expires at, written by
Engine::claim_replay_marker(). A row with a non-numeric value casts to 0
and is therefore treated as expired — same fail-open-on-garbage behaviour as
the sweep, and no caller outside the engine ever writes these rows.
Number of option rows removed.
Runs the per-IP rate limiter. Hooked on `init` at priority 0; registered after the firewall so the firewall runs first.
creationell_captcha_run_rate_limiter() : void
Returns the path portion of the current request, the way WordPress core derives it — NOT the way a URL parser would.
creationell_captcha_request_path() : string
The return value is still percent-encoded (the "wire spelling"); callers that
need the decoded form apply rawurldecode() to it exactly once, after this
function has split off the query.
Every call site used to run wp_parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH ).
wp_parse_url() is a URL parser, and REQUEST_URI is not a URL — it is a
request target. For a target that starts with // the parser treats the
string as a scheme-relative URL (it prepends placeholder:, see
wp-includes/http.php), so the first segment becomes the HOST:
REQUEST_URI wp_parse_url(…, PHP_URL_PATH) WP::parse_request()
/kontakt/ '/kontakt/' kontakt
//kontakt/ '/' kontakt ← same page
///kontakt/ '' kontakt ← same page
Core reduces the target in WP::parse_request() with
list( $req_uri ) = explode( '?', $_SERVER['REQUEST_URI'] ); $req_uri = trim( $req_uri, '/' );
(wp-includes/class-wp.php) and therefore routes all three spellings to the
same page. The guards saw / or '', matched no pattern, and let the
request through: POST //kontakt was a one-character bypass of the entire
interceptor path guard.
? inside the path cannot smuggle a query string in.POST http://example.com/kontakt HTTP/1.1, RFC 9112 §3.2.2). Core does
not do this and simply 404s such a request, but wp_parse_url() did — so
keeping it guards MORE than core routes, never less. Dropping it would
have taken protection away that 1.0.2 had.trim( $req_uri, '/' ) performs. INNER double slashes are kept, because
core keeps them too, and the TRAILING slash is kept, because existing
patterns are written against the spelling wp_parse_url() produced.Request path with exactly one leading slash, still encoded.
Der aktuelle Request als relativer Ziel-URI: Pfad aus `creationell_captcha_request_path()`, Query aus derselben Zeichenkette.
creationell_captcha_request_target() : string
remove_query_arg() OHNE URL-Argument und add_query_arg() ohne URL-
Argument sind dieselbe Mechanik: beide delegieren an add_query_arg(), und
dessen Zweig count( $args ) < 3 (bzw. < 2 bei der Array-Form) liest
$_SERVER['REQUEST_URI'] ROH. Sie leiten also eine Pfadangabe aus dem
Request ab, ohne das sichtbar zu tun — deshalb standen die betroffenen
Stellen in keinem der vier Review-Berichte.
Gemessen wurde das am Ausblenden-Link des Härtungs-Hinweises: bei
REQUEST_URI = "//wp-admin/admin.php?page=…" rendert er ein
protokollrelatives href="//wp-admin/admin.php?…"; esc_url() reicht jede
mit / beginnende Zeichenkette unverändert durch. Der Browser liest das als
Host wp-admin, der Klick kommt nie an. Dieselbe Konstruktion stand am
„↻ Aktualisieren"-Link der Statistik-Seite.
Die Query kommt bewusst aus DERSELBEN Zeichenkette wie der Pfad, damit
beide Hälften zusammenpassen und nicht die eine aus REQUEST_URI und die
andere aus QUERY_STRING stammt (die beiden können auseinanderlaufen, etwa
hinter einer Rewrite-Regel). Und der Rückgabewert ist bewusst RELATIV: er
ersetzt genau das, was der Kern ohne URL-Argument selbst eingesetzt hätte.
VOLLZÄHLIGKEIT: Seit der Nachlese N6 übergibt jeder
add_query_arg()/remove_query_arg()-Aufruf im ausgelieferten Code eine
explizite URL — entweder diese Funktion, eine $base_url aus
menu_page_url() oder admin_url(). Nachgezählt wird das nicht von Hand,
sondern von tests/test-hardening-migration.php (Abschnitt „Bestandsaufnahme"),
das alle Aufrufe unter includes/ auszählt und rot wird, sobald einer wieder
ohne URL-Argument dasteht. Ein früherer Stand dieses Docblocks nannte die
Redirect-Funktion der Härtungs-Migration „die neunte und letzte Stelle im
Plugin, die eine Pfadangabe aus dem Request ableitet" — das war messbar
falsch und ist hiermit ersetzt.
Relativer Ziel-URI (führender Schrägstrich, genau einer).
Returns the REST route the current request actually addresses.
creationell_captcha_current_rest_route() : string
Returns a normalised route (leading slash, no trailing slash, no query
part) — for example /creationell-captcha/v1/challenge — or the empty
string when the request is not served by the REST API.
The function is deliberately conservative: whenever it cannot establish that WordPress core will hand the request to the REST server, it returns the empty string. Both consumers fail closed on that answer (the interceptor guards the request, the rate limiter counts it), so under-detection is safe while over-detection hands out a bypass.
It must work at init priority 0/1, i.e. long before parse_request has
populated $GLOBALS['wp']->query_vars and before core defines the
REST_REQUEST constant, so it reconstructs core's own decision from the
request instead of reading either of those.
Normalised REST route, or '' when this is not a REST request.
Normalises a raw `rest_route` value into the plugin's canonical route form.
creationell_captcha_normalize_rest_route(string $raw) : string
Returns '' for every value that does not address a concrete REST route.
Raw rest_route value as core would see it.
Route with a leading slash and without a trailing one, or ''.
Registers the public challenge route.
creationell_captcha_register_rest_routes() : void
REST-GATE-OK: /challenge is deliberately public (permission_callback __return_true) — it is the shared entry point that issues a fresh, single-use PoW challenge for ALL captcha types, not a feature specific to one toggle. It remains subject to the global kill switch (creationell_captcha_is_disabled()) below.
Returns a fresh, single-use challenge. Records the issuance via the standard event channel — aggregate counters always increment, the detail-log entry is gated by the `log_challenge` per-type toggle from Modul 11c.
creationell_captcha_rest_challenge(WP_REST_Request $request) : WP_REST_Response
The REST request. Optional query parameter
ctx (single-use, 120-second token the
under-attack interstitial passes to suppress
the code-challenge attachment). A challenge
issued for an accepted ctx is marked
under-attack-only inside its signed
parameters and is worthless at a protected
form — see Engine::verify().
JSON challenge envelope with algorithm, challenge,
salt, signature, parameters and optional
codeChallenge.image URL.
Canonical-JSON serialisation of ALTCHA challenge parameters, byte- identical to `altcha-lib-php`'s `ChallengeParameters::toCanonicalJson()` (= ksort top-level + recursive ksort on assoc sub-arrays, JSON-encoded with UNESCAPED_SLASHES | UNESCAPED_UNICODE, null keys dropped).
creationell_captcha_canonical_params_json(array<string, mixed> $params) : string
Needed for the re-sign step in Modul 15's /challenge handler after we mutate parameters.data.ccode.
Parameter array from create_challenge().
Recursive helper used by `canonical_params_json` — mirrors the lib's `sortRecursive`. List arrays (sequential integer keys) keep their order; associative arrays get `ksort`-ed in place.
creationell_captcha_canonical_sort_recursive(array<string|int, mixed> &$data) : void
Setting keys whose value is a list (every `textarea` field).
creationell_captcha_list_setting_keys() : array<int, string>
Derived from the field specification so the list never drifts. "Load defaults" preserves these keys; "reset" clears them.
Builds the settings-export payload.
creationell_captcha_export_settings() : array<string, mixed>
The HMAC secrets are deliberately excluded — they must never leave the site.
Validates and applies a settings-export payload.
creationell_captcha_import_settings(array<string, mixed> $payload) : array<string, mixed>|WP_Error
The settings array is run through creationell_captcha_sanitize_settings(),
so the same guarantees as the settings form apply: whitelisted selects,
clamped numbers, bounded lists, unknown keys dropped, missing keys defaulted.
Decoded export payload.
On success: { imported, version_notice, source_notice }.
Full factory reset: writes the complete default settings array, which also empties every list and drops stored keys outside the current field specification. Secrets, analytics counters and the event log are left untouched.
creationell_captcha_reset_settings() : void
Resets every non-list setting to its default while preserving the current list values (IP block/allow, UA block, interceptor paths).
creationell_captcha_load_default_settings() : void
Die übernommenen Listen laufen dabei durch dieselbe Eingabeprüfung wie ein
regulärer Speichervorgang (E3a): ein Eintrag, der an den Plugin-Schreibwegen
vorbei in die Option kam (wp option update, DB-Restore), überlebte
„Standardwerte laden" bisher ungeprüft — auch dann, wenn die Prüfung ihn
heute ablehnt.
Ordered list of the admin settings tabs.
creationell_captcha_admin_tabs() : array<string, string>
The array key is the tab id; it doubles as the suffix of the Settings-API
page slug (creationell-captcha-tab-<id>) that do_settings_sections() uses.
Tab id => visible label.
Settings sections and the tab each one belongs to.
creationell_captcha_admin_sections() : array<string, array<string, string>>
Section order within a tab follows this array's order.
Section id => { tab, title, callback }.
Field specification for the captcha settings.
creationell_captcha_settings_fields() : array<string, array<string, mixed>>
Every field carries a section key naming the section (and thereby the tab)
it renders in; the section ids match creationell_captcha_admin_sections().
Registers the plugin setting, the per-tab sections and the fields.
creationell_captcha_register_settings() : void
Liest — und setzt optional — den angehefteten Sanitizer-Kontext.
creationell_captcha_sanitize_context([string|false|null $set = false ]) : string|null
Der Kontext wird bewusst EXPLIZIT gesetzt und nicht aus is_admin(),
admin_init oder ähnlichen Umgebungsindizien erraten: genau dieses Raten ist
die Wurzel der Kartenwidersprüche W1/W2 der Befunddatei (ein Menüpunkt, zwei
Semantiken, je nachdem ob admin_init gefeuert hat).
FALSE liest nur; ein String oder NULL setzt.
Aktuell angehefteter Kontext oder NULL.
Führt $callback aus, während der Sanitizer-Kontext auf $context festgelegt ist.
creationell_captcha_with_sanitize_context(string $context, callable $callback) : mixed
Nötig, weil register_setting() den Sanitizer zusätzlich als
sanitize_option_creationell_captcha_settings-Filter anhängt: jeder
update_option()-Aufruf im Admin-Kontext läuft durch ihn hindurch, auch
Import und Reset über admin-post.php. Ohne diese Klammer bekämen genau
jene Pfade Formular-Semantik.
Einer der CREATIONELL_CAPTCHA_SANITIZE_*-Werte.
Auszuführender Schreibvorgang.
Rückgabewert von $callback.
Einstiegspunkt des `sanitize_option_creationell_captcha_settings`-Filters.
creationell_captcha_sanitize_settings_option(mixed $input) : array<string, mixed>
Rohwert aus dem Schreibvorgang.
Kürzt einen Einstellungswert auf höchstens $max_chars Zeichen und garantiert gültiges UTF-8.
creationell_captcha_truncate_setting_text(string $value, int $max_chars) : string
Bewusst zeichen- statt byteorientiert: substr() schnitt bisher mitten in
eine Mehrbyte-Sequenz (AF-7). Ungültiges UTF-8 wird entfernt, bevor der Wert
in den serialisierten Options-Blob wandert — nicht danach in der DB repariert.
Bereits vorsanitisierter Wert.
Obergrenze in Zeichen (nicht Bytes).
Kürzt gültiges UTF-8 ohne mbstring auf $max_chars ZEICHEN.
creationell_captcha_truncate_chars(string $value, int $max_chars) : string
Drei Stufen, weil jede einzelne ausfallen kann — und der Ausfall darf den Wert nicht kosten:
preg_match( '/^.{0,N}/us' ) — der schnelle Weg. s lässt „.“ auch
Zeilenumbrüche treffen (relevant für textblock).iconv_substr() — greift, wenn Stufe 1 an einem PCRE-Limit scheitert
(Backtrack-Limit, JIT-Stack). iconv ist keine PCRE-Bibliothek und
deshalb von diesen Limits nicht betroffen.Nach Stufe 1 stand hier bis zur Nachlese return '';. Das war als
„lieber ein leeres Feld, das der Betreiber sieht" begründet — nur sieht er
es nicht: an dieser Stelle wird keine add_settings_error() erzeugt, das
leere Feld ist also genau so still wie die kaputte Sequenz, gegen die die
Begründung argumentierte. Und der Sanitizer läuft bei JEDEM Schreibvorgang,
auch bei einem, der dieses Feld gar nicht anfasst (Werksreset,
„Standardwerte laden", settings set eines anderen Schlüssels, jedes
Speichern der Einstellungsseite): ein gespeicherter CSS- oder
Übersetzungsblock wäre damit still und vollständig weg gewesen. Der
Vorgängerstand vor W2-7 lieferte an dieser Stelle wenigstens den Anfang des
Wertes (substr(), byteweise und deshalb möglicherweise mitten in einer
Mehrbyte-Sequenz). Beide Stufen unten liefern denselben Anfang, nur an einer
ZEICHEN-Grenze — der Datenverlust entfällt, ohne den AF-7-Fehler
zurückzuholen.
Vorbedingung: $value ist gültiges UTF-8. Der Aufrufer stellt das her; Stufe 3 verlässt sich darauf, weil sie Startbytes zählt und Fortsetzungsbytes (0x80–0xBF) in gültigem UTF-8 nie an einer Zeichengrenze stehen.
Gültiges UTF-8.
Obergrenze in Zeichen.
Dritte und letzte Kürzungsstufe: zählt UTF-8-Startbytes.
creationell_captcha_truncate_chars_by_bytes(string $value, int $max_chars) : string
Braucht weder mbstring noch iconv noch PCRE — deshalb steht sie am Ende der
Kette in creationell_captcha_truncate_chars(). Eigene Funktion, weil sie
sonst auf jedem Rechner mit iconv unerreichbar und damit ungetestet wäre.
Startbytes: 0xxxxxxx = 1 Byte, 110xxxxx = 2, 1110xxxx = 3,
11110xxx = 4. Fortsetzungsbytes (10xxxxxx, 0x80–0xBF) stehen in gültigem
UTF-8 nie an einer Zeichengrenze; ungültige Eingaben schliesst der Aufrufer
aus (creationell_captcha_truncate_setting_text() prüft und repariert vorher).
Gültiges UTF-8.
Obergrenze in Zeichen.
Sanitises the settings array before it is stored.
creationell_captcha_sanitize_settings(mixed $input[, string $context = CREATIONELL_CAPTCHA_SANITIZE_PROGRAMMATIC ]) : array<string, mixed>
Raw input from the settings form.
Schreibkontext, einer der
CREATIONELL_CAPTCHA_SANITIZE_*-Werte. Default ist
„programmatisch": alle direkten Aufrufer (Import,
WP-CLI) übergeben einen vollständigen Wertesatz.
Ausgewertet werden ausschließlich die zwei bekannten
programmatischen Werte …_PROGRAMMATIC und …_RESET;
jeder andere Wert — auch ein unerwarteter, etwa der
Optionsname aus einer Legacy-sanitize_option-
Registrierung — erhält die verlustfreie
Formular-Semantik. …_RESET unterscheidet sich von
…_PROGRAMMATIC an genau einer Stelle: er übernimmt
auch keine gespeicherten Schlüssel ausserhalb der
aktuellen Feldspezifikation (voller Werksreset).
Legt die Ereignis-Log-Tabelle an, sobald das Detail-Log eingeschaltet GESPEICHERT wurde.
creationell_captcha_sync_event_log_table() : void
E3b — warum das hier hängt und nicht mehr im Sanitizer: Der Sanitizer ist
über register_setting() zugleich der sanitize_option-Filter der Option
und eine öffentliche, dokumentierte Funktion. Ein ensure_table() in ihm
bedeutete DDL für jeden Aufrufer, der nur prüfen, vergleichen oder eine
Vorschau bauen will — wp creacaptcha doctor verzichtet deshalb bereits
ausdrücklich auf den Sanitizer-Vergleich (Check 9). Die Tabellenanlage
gehört an das Ergebnis des Schreibvorgangs; das ist dieselbe Bauform, die
creationell_captcha_sync_cloudflare_cron() für den Cron-Slot benutzt.
Reichweite gegenüber vorher:
admin_init: NEU abgedeckt. Dort hing
der sanitize_option-Filter gar nicht; wp option update creationell_captcha_settings konnte das Log einschalten, ohne dass je eine
Tabelle entstand (die Selbstheilungs-Lücke aus DS-4).creationell_captcha_store_settings() ab — jeder
Plugin-eigene Schreibweg auf die Option ruft die Synchronisierung danach
selbst noch einmal auf. Nicht abgedeckt bleibt ein Schreibvorgang an
diesen Wegen vorbei (wp option update, DB-Restore) ohne Wertänderung;
dafür bleibt wp creacaptcha repair der Weg, auf den Doctor-Check 3 und
der Log-Hinweis in Analytics::log_row() verweisen.DS-2-Grenze: Auslöser ist ein bereits erfolgter Schreibvorgang auf die
Plugin-Option. Anders als beim admin_init-Aufhänger der Migration in
upgrade.php gibt es hier keinen anonym erreichbaren Pfad — wer die Option
schreiben kann, hat die Site ohnehin in der Hand.
Der eine Schreibweg des Plugins auf `creationell_captcha_settings`.
creationell_captcha_store_settings(array<string, mixed> $clean) : void
Warum es ihn gibt (Nachlese N6, Befund 4): update_option() feuert
update_option_{$option} NUR, wenn sich der gespeicherte Wert wirklich
ändert (wp-includes/option.php vergleicht alt/neu und bricht sonst ohne
DB-Schreibvorgang und ohne Hook ab). Seit E3b hängt die Anlage der
Ereignis-Tabelle ausschliesslich an diesen Hooks — ein Schreibvorgang ohne
Wertänderung ist damit ein Erfolg ohne Wirkung:
wp creacaptcha settings set analytics_event_log 1 # steht schon auf 1
wp creacaptcha blocklist add 203.0.113.9 # steht schon drin
meldeten Erfolg, während eine von Hand gelöschte Tabelle (DB-Restore,
Migration) weg blieb und jedes Ereignis still verworfen wurde. Strang N2
hatte das für Command::enable() einzeln kompensiert; der zweite und dritte
CLI-Weg auf denselben Schlüssel hatten die Kompensation nicht. Statt sie an
jeder Stelle zu wiederholen, steht sie jetzt EINMAL hier, und alle
Plugin-eigenen Schreibwege gehen hindurch.
Nachgerufen wird nur, wenn update_option() FALSE liefert — also genau
dann, wenn der Hook nicht gefeuert hat. ensure_table() ist zwar
idempotent, aber nicht kostenlos (dbDelta liest das Schema), und ein
zweiter Durchlauf bei jedem echten Speichervorgang wäre reine Wiederholung.
Der FALSE-Zweig deckt beide Ursachen ab: unveränderter Wert und
fehlgeschlagener Schreibvorgang.
Bereits sanitisierter Wertesatz.
Renders the description shown at the top of the Proof-of-Work-Engine section.
creationell_captcha_render_engine_section() : void
Renders the description shown at the top of the widget-appearance section.
creationell_captcha_render_widget_appearance_section() : void
Renders the description shown at the top of the code-challenge section.
creationell_captcha_render_code_challenge_section() : void
Includes a warning notice when the PHP-GD extension is missing — without it, image rendering cannot work and the trigger logic stays disabled.
Renders the description shown at the top of the core-forms section.
creationell_captcha_render_core_forms_section() : void
Renders the description shown at the top of the interceptor section.
creationell_captcha_render_interceptor_section() : void
Renders the description shown at the top of the form-plugins section.
creationell_captcha_render_form_plugins_section() : void
When no supported form plugin is active the section has no fields, so the description doubles as a hint.
Renders the description shown at the top of the proxy section.
creationell_captcha_render_proxy_section() : void
Renders the description shown at the top of the bypass section.
creationell_captcha_render_bypass_section() : void
Renders the description shown at the top of the firewall section.
creationell_captcha_render_firewall_section() : void
Renders the description shown at the top of the rate-limiting section.
creationell_captcha_render_ratelimit_section() : void
Renders the description shown at the top of the under-attack section.
creationell_captcha_render_underattack_section() : void
Renders the description shown at the top of the under-attack appearance section.
creationell_captcha_render_underattack_appearance_section() : void
Renders the description shown at the top of the analytics section.
creationell_captcha_render_analytics_section() : void
Renders the description shown at the top of the email-protection section.
creationell_captcha_render_email_section() : void
Renders a single settings field.
creationell_captcha_render_field(array<string, mixed> $args) : void
Field arguments (key + field spec).
Liefert das Label eines Einstellungsfeldes (Fallback: der Schlüssel selbst).
creationell_captcha_field_label(string $key) : string
Feldschlüssel.
Request-lokal gehaltene Feldspezifikation für die beiden Label-Helfer.
creationell_captcha_field_spec() : array<string, array<string, mixed>>
W2-8: Beide hielten je einen wortgleichen static $spec-Block und damit zwei
unabhängige Kopien der ~75-Felder-Spezifikation im Speicher. Eine gemeinsame
Zugriffsfunktion genügt — und sie macht zugleich sichtbar, dass beide
denselben Stand meinen.
Liefert das Options-Label eines `select`-Feldes (Fallback: der Rohwert).
creationell_captcha_field_option_label(string $key, string $value) : string
Feldschlüssel.
Optionswert.
Stores a one-shot admin notice and redirects back to the Werkzeuge page.
creationell_captcha_tools_redirect(string $type, string $message) : never
'success' or 'error'.
Notice text.
Guards a tools action: requires manage_options and a valid nonce.
creationell_captcha_tools_guard(string $action) : void
The nonce action name.
Streams the current settings as a JSON download.
creationell_captcha_handle_export_settings() : void
Handles the settings-import upload.
creationell_captcha_handle_import_settings() : void
Collects the plugin's queued settings-error messages, de-duplicated.
creationell_captcha_collect_settings_error_messages() : array<int, string>
Der Sanitizer läuft auf dem Importweg zweimal (einmal explizit, einmal über
den sanitize_option-Filter in update_option()), meldet identische Funde
also doppelt — deshalb array_unique.
Handles the full factory reset.
creationell_captcha_handle_reset_settings() : void
Handles "load defaults" (keeps the lists).
creationell_captcha_handle_load_defaults() : void
Triggers a manual Cloudflare-range refresh from the Werkzeuge page.
creationell_captcha_handle_cloudflare_refresh() : void
Empties the cached Cloudflare-range option from the Werkzeuge page.
creationell_captcha_handle_cloudflare_clear() : void
Registers the "Werkzeuge" submenu page under the CreaCaptcha menu.
creationell_captcha_register_tools_page() : void
Renders the one-shot admin notice left behind by a tools action.
creationell_captcha_render_tools_notice() : void
Renders the "Werkzeuge" page.
creationell_captcha_render_tools_page() : void
Renders the Cloudflare-cache status block inside the Werkzeuge tool card.
creationell_captcha_render_cloudflare_status() : void
Runs the under-attack interstitial gate for front-end page views. Hooked on `template_redirect` — fires only for front-end requests, so wp-admin, wp-login.php, REST and cron are inherently exempt.
creationell_captcha_run_under_attack() : void
Runs schema migrations when the stored version differs from the running one.
creationell_captcha_maybe_upgrade() : void
Hooked on admin_init and gated twice — see the inline notes on DS-2. When
the event-log table already exists it is re-run through dbDelta so new
columns are added; a missing table is created only when
analytics_event_log says it should exist (DS-4).
Migrates the legacy `widget_mode` setting (Modul 11a) to the new `widget_display` + `widget_auto_trigger` pair (Modul 14). Idempotent — if `widget_display` is already present in the stored option, the migration is skipped.
creationell_captcha_migrate_widget_mode() : void
Mapping: visible → widget_display=standard, widget_auto_trigger=none auto → widget_display=invisible, widget_auto_trigger=onload overlay → widget_display=floating, widget_auto_trigger=onsubmit
Den Alt-Schlüssel widget_mode nimmt die Migration aus dem Wertesatz, den
sie übergibt; ab Modul 14 wird er nirgends mehr gelesen. Ob er damit aus der
Option verschwindet, entscheidet der Sanitizer: Er trägt am Ende jeden
Schlüssel nach, der zwar gespeichert ist, aber nicht in der
Feldspezifikation steht (Carry-over für den Toggle eines gerade inaktiven
Formular-Plugins) — und er liest dafür den Stand VOR diesem Schreibvorgang.
Im wp-admin, wo er zusätzlich als sanitize_option-Filter hängt, kommt
widget_mode deshalb zurück. Folgenlos: keine Codestelle wertet den
Schlüssel noch aus, und creationell_captcha_get_settings() merged ohnehin
über die Defaults. Aus demselben Grund ist der Aufräum-Zweig unten
(widget_display vorhanden, widget_mode noch da) dort ein Schreibvorgang
ohne Änderung.
Writes a migrated settings array back — through the sanitiser, with the write context pinned.
creationell_captcha_store_migrated_settings(array<string, mixed> $settings) : void
B-M6: Die Migration schrieb den rohen Options-Inhalt zurück. Heute ist das
folgenlos (der Pfad läuft nur im wp-admin, wo der sanitize_option-Filter
hängt, und die gesetzten Werte stammen aus einer Konstantentabelle), aber es
ist derselbe Bauart-Fehler wie in settings-manager.php: Ob sanitisiert
wird, hing am Request-Kontext statt am Aufrufer. Der Kontext ist
PROGRAMMATIC — der übergebene Wertesatz ist vollständig und bewusst gewählt,
die requires-Rückschreibung der Formular-Semantik würde eine vom Sanitizer
verworfene Eingabe wieder einsetzen.
Was diese Funktion NICHT leistet: einen Schlüssel aus der Option entfernen.
Die Nachtrags-Schleife am Ende des Sanitizers setzt jeden gespeicherten
Schlüssel wieder ein, der nicht in der Feldspezifikation steht — sie liest
dabei den Stand VOR diesem Schreibvorgang. Ein unset() hier würde nur in
einem Request ohne sanitize_option-Filter wirken und genau die
Kontextabhängigkeit wiederherstellen, die dieser Fix beseitigt. Der
Alt-Schlüssel widget_mode bleibt deshalb gegebenenfalls stehen; er wird
nirgends mehr gelesen (siehe Docblock der Migration).
Migrierter Wertesatz.
Registers the widget script and — for Argon2id — its worker registration.
creationell_captcha_register_assets() : void
Macht eine gespeicherte CSS-Zeichenkette sicher für die Ausgabe in einem `<style>`-Element.
creationell_captcha_safe_inline_css(string $css) : string
Gemeinsame Ausgabe-Härtung für beide Stellen, an denen Nutzer-CSS in einen
<style>-Block geschrieben wird: das Widget (widget_custom_css) und die
Under-Attack-Interstitial-Seite (underattack_custom_css).
Warum an der AUSGABE und nicht (nur) im Schreibpfad: Der Sanitizer räumt
widget_custom_css/underattack_custom_css zwar per wp_strip_all_tags() auf,
aber mindestens drei Schreibwege erreichen ihn nie — wp option patch, fremde
update_option()-Aufrufe außerhalb des Admin-Kontexts und die eigenen
CLI-Befehle reset/load-defaults (W2 der Befunddatei). Was in der Option
steht, ist also nicht garantiert sanitisiert; die Ausgabe muss unabhängig
davon sicher sein.
Rohes CSS aus den Einstellungen.
CSS, das den umgebenden <style>-Block nicht verlassen kann.
Builds the ALTCHA widget markup as a plain string. Enqueues the widget script as a side effect.
creationell_captcha_build_widget_markup() : string
Safe to call from inside an ob_start callback because it does not use
output-buffering itself — unlike the legacy creationell_captcha_get_widget_markup
wrapper that this function now powers.
Reads eight widget-customization settings (display, type, auto_trigger, theme, hide_branding, primary_color, custom_css, strings_override) and maps them to the corresponding v3 attributes. Boolean attributes are emitted as empty-string values per HTML5 convention.
Renders the ALTCHA widget markup and enqueues its assets.
creationell_captcha_render_widget() : void
Verifies a raw base64 ALTCHA payload string.
creationell_captcha_verify_payload(string $raw) : bool
Shared by the POST-based request helper and the third-party form integrations, which read the payload from their plugin's submission data.
The raw altcha payload.
Reads and verifies the ALTCHA payload from the current POST request.
creationell_captcha_verify_request() : bool
The ALTCHA payload itself is the anti-bot token — no separate WordPress nonce applies here.
Public template tag — renders the ALTCHA widget.
creationell_captcha_widget() : void
For use in theme templates or custom-form markup; the call must sit inside the
Returns the ALTCHA widget markup as a string.
creationell_captcha_get_widget_markup() : string
Used by the shortcode and by the third-party form integrations, which embed the widget into another plugin's form markup. Implementation routes through the underlying string builder rather than ob_start so the function is safe to call from inside other output-buffer callbacks (e.g. Modul 12's auto-inject buffer).
The widget markup.
Shortcode handler for [creationell_captcha].
creationell_captcha_widget_shortcode() : string
Place the shortcode inside a
The widget markup.
Removes every trace of the plugin from the site that is currently switched to: options, replay markers, transients, cron slots and the event-log table.
creationell_captcha_uninstall_site() : void
Reads $wpdb freshly on each call because switch_to_blog() repoints
$wpdb->prefix and $wpdb->options at the other site's tables.
Removes the plugin's per-user data.
creationell_captcha_uninstall_user_meta() : void
B-M4: the hardening notice introduced in this release remembers per
administrator that it was dismissed. Without this, one wp_usermeta row per
administrator who ever clicked the notice away survives the uninstall — and
the routine above promises to remove "every trace".
Deliberately NOT part of creationell_captcha_uninstall_site(): wp_usermeta
is a network-global table that switch_to_blog() does not repoint, so this
runs exactly once for the whole installation. delete_metadata() with
$delete_all = true ignores the object id and removes the key for every user
in a single statement.