CreaCaptcha

Captcha

Table of Contents

Packages

GitUpdate

Classes

Analytics
Records security events into per-day and per-hour aggregate counters and, when the detailed event log is enabled, into a dedicated database table.
EmailObfuscator
Obfuscates mailto: links and plain-text email addresses in front-end output. The real address is XOR-hex encoded into a data-cce attribute and restored client-side by the decoder script. Two modes share this class: the content-filter mode (process()) and the full-page-buffer mode (process_page()). See the module-7 and module-8 design specs.
Engine
Wraps the bundled ALTCHA library for the WordPress plugin.
Firewall
Blocks requests by client IP or user-agent before WordPress processes them.
Interceptor
Inspects front-end POST requests and enforces a valid ALTCHA payload for requests whose path matches a configured pattern. See the module-2 design spec for the bypass chain (§6) and the guard decision (§7/§8).
RateLimiter
Counts requests per client IP in a fixed time window and blocks with HTTP 429 once the configured limit is exceeded. See the module-4 design spec §6.
UnderAttack
Gates anonymous front-end page views behind an interstitial proof-of-work challenge while under-attack mode is active. See the module-5 design spec.
Cloudflare_Command
`wp creacaptcha cloudflare …` — refresh, clear and status for the cached Cloudflare IP-range list.
Command
Inspects and controls CreaCaptcha from the command line.
Doctor_Command
`wp creacaptcha doctor`
List_Command
Manages one list-type setting — IP block/allow list, UA block list or the interceptor path list. The same class backs the blocklist, allowlist, ua-blocklist and paths command namespaces.
Log_Command
Inspects and maintains the optional event-log table.
Settings_Command
Reads, writes, exports, imports and resets CreaCaptcha settings.
Test_Bypass_Command
`wp creacaptcha test-bypass`

Constants

CREATIONELL_CAPTCHA_EXPORT_MAX_ROWS  = 50000
The default upper bound on rows in a single CSV export.
CREATIONELL_CAPTCHA_EXPORT_SCHEMA  = 1
Schema version of the settings-export format.
CREATIONELL_CAPTCHA_HARDENING_DISMISS_ARG  = 'creationell_captcha_hardening_dismiss'
Query-Parameter des Ausblenden-Links.
CREATIONELL_CAPTCHA_HARDENING_DISMISS_META  = 'creationell_captcha_hardening_dismissed'
User-Meta-Schlüssel, unter dem ein Benutzer den Hinweis wegklickt.
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', ]
Maps WordPress locales to the matching ALTCHA i18n locale bundle.
CREATIONELL_CAPTCHA_PURPOSE_UA_CTX  = 'underattack-ctx'
Purpose label of the under-attack code-challenge suppression token (`ctx`).
CREATIONELL_CAPTCHA_PURPOSE_UA_PASS  = 'underattack-pass'
Purpose label of the under-attack pass-cookie key.
CREATIONELL_CAPTCHA_SANITIZE_FORM  = 'form'
Sanitiser context: the settings form (`options.php`).
CREATIONELL_CAPTCHA_SANITIZE_PROGRAMMATIC  = 'programmatic'
Sanitiser context: programmatischer Schreibvorgang (Import, Werksreset, „Standardwerte laden", WP-CLI).
CREATIONELL_CAPTCHA_SANITIZE_RESET  = 'reset'
Sanitiser context: Werksreset (`creationell_captcha_reset_settings()`).
CREATIONELL_CAPTCHA_UA_CTX_TTL  = 120
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).

Functions

creationell_captcha_register_admin_menu()  : void
Registers the top-level "CreaCaptcha" admin menu entry.
creationell_captcha_render_settings_page()  : void
Renders the tabbed settings page.
creationell_captcha_render_import_warning()  : void
Renders the "an import replaces everything" warning on the Werkzeuge page.
creationell_captcha_render_trust_notice()  : void
Renders the "proxy mode on but trust-set empty" admin notice on plugin pages.
creationell_captcha_tabbed_page_hooks()  : array<int, string>
Records and reports the admin-page hook suffixes of the plugin's tab-organised pages.
creationell_captcha_active_tab()  : string
Determines the active tab from the request, whitelisted against $tabs.
creationell_captcha_render_nav_tabs()  : void
Renders the no-JavaScript fallback style and the nav-tab bar.
creationell_captcha_csv_cell()  : string
Neutralises a CSV cell against spreadsheet formula injection.
creationell_captcha_export_events()  : void
Streams the filtered event log as a CSV download.
creationell_captcha_register_analytics_page()  : void
Registers the "Statistik" submenu page under the CreaCaptcha menu.
creationell_captcha_analytics_labels()  : array<string, string>
Human-readable labels for the seven event types.
creationell_captcha_analytics_groups()  : array<int, array{title: string, description: string, types: array}>
Thematic groups for the overview tab: title, explanation and the member event types with their short in-group labels.
creationell_captcha_analytics_tabs()  : array<string, string>
The three tabs of the analytics page.
creationell_captcha_sum_recent_days()  : array<string, int>
Sums the $days most recent day buckets, per event type.
creationell_captcha_sum_recent_hours()  : array<string, int>
Sums the $hours most recent hour buckets, per event type.
creationell_captcha_render_analytics_page()  : void
Renders the tabbed analytics dashboard page.
creationell_captcha_render_analytics_overview()  : void
Renders the "Übersicht" tab: four 24-hour KPI tiles and one four-window comparison table per thematic group.
creationell_captcha_render_analytics_history()  : void
Renders the "Verlauf" tab: the day-by-day table for the last 30 days.
creationell_captcha_events_query_args()  : array{search: string, event_type: string, date_from: string, date_to: string}
Reads and sanitises the event-log filter from the request.
creationell_captcha_render_analytics_events()  : void
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_event_modal()  : void
Renders the (initially hidden) event-detail modal skeleton.
creationell_captcha_render_events_toolbar()  : void
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_pagination()  : void
Renders the pagination navigation below the event-log table.
creationell_captcha_analytics()  : Analytics
Returns the shared analytics recorder instance.
creationell_captcha_record_event()  : void
Records a security event of the given type.
creationell_captcha_enqueue_admin_assets()  : void
Enqueues admin styles and scripts on the CreaCaptcha admin pages.
creationell_captcha_cloudflare_snapshot()  : array{v4: string[], v6: string[], updated_at: string}
Returns the bundled Cloudflare snapshot.
creationell_captcha_cloudflare_ranges()  : array<string|int, string>
The active CF range list: cached option (if fresh and valid) → bundled snapshot.
creationell_captcha_refresh_cloudflare_ips_now()  : array{ok: bool, v4: int, v6: int, fetched_at: int|null, error: string|null}
Fetches the live Cloudflare ranges and writes them to the cache option.
creationell_captcha_clear_cloudflare_cache()  : bool
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_fetch_cloudflare_list()  : array<string|int, string>
Fetches one Cloudflare endpoint and returns the valid CIDR entries.
creationell_captcha_sync_cloudflare_cron()  : void
Ensures the daily refresh cron slot is in sync with the auto-refresh toggle.
creationell_captcha_code_challenge_font_usable()  : bool
Probes whether GD can actually render text with the vendored TTF font.
creationell_captcha_render_code_image()  : string
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_should_issue_code_challenge()  : bool
Decides whether the current /challenge request should attach a code-challenge instruction. Returns true iff:
creationell_captcha_code_charset()  : string
Returns the active charset string for the configured option.
creationell_captcha_generate_code()  : string
Generates a fresh random code from the configured charset.
creationell_captcha_code_token_issue()  : 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_verify()  : string|null
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_fail_key()  : string
Transient name of the per-token failed-attempt counter (CM-3).
creationell_captcha_code_token_fail()  : void
Records one wrong image-code submission for a token and invalidates the token once its attempt budget is spent.
creationell_captcha_rest_code_image()  : WP_REST_Response
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_verify()  : WP_REST_Response
Handles POST /code-verify. Two body shapes:
creationell_captcha_register_code_challenge_routes()  : void
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_email_obfuscation()  : 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_buffer()  : 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_email_buffer_filter()  : string
The buffer callback itself: decides whether this response gets rewritten and hands it to the obfuscator when it does.
creationell_captcha_response_is_html()  : bool
Whether the response being buffered is (still) an HTML document.
creationell_captcha_run_firewall()  : void
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_comments_active()  : bool
Whether comment protection applies to the current request.
creationell_captcha_comments_render()  : string
Injects the widget just above the comment form submit button.
creationell_captcha_comments_verify()  : array<string, mixed>
Verifies the captcha before a comment is accepted.
creationell_captcha_login_enabled()  : bool
Whether login protection is enabled.
creationell_captcha_login_render()  : void
Renders the widget inside the login form.
creationell_captcha_login_form_middle()  : string
Renders the widget inside `wp_login_form()`-based forms.
creationell_captcha_login_is_interactive()  : bool
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_verify()  : WP_User|WP_Error|null
Verifies the captcha during an interactive login.
creationell_captcha_password_reset_enabled()  : bool
Whether password-reset protection is enabled.
creationell_captcha_password_reset_render()  : void
Renders the widget inside the lost-password form.
creationell_captcha_password_reset_exempt_reason()  : string|null
Why the current request is not a public lost-password form submission.
creationell_captcha_password_reset_exempt_label()  : string
Human-readable label for an exemption reason, for the event log.
creationell_captcha_password_reset_verify()  : void
Verifies the captcha during a password-reset request.
creationell_captcha_registration_enabled()  : bool
Whether registration protection is enabled.
creationell_captcha_registration_render()  : void
Renders the widget inside the registration form.
creationell_captcha_registration_verify()  : WP_Error
Verifies the captcha during registration.
creationell_captcha_hardening_covered_family()  : string
Ermittelt, WELCHE Adressfamilie ein gespeicherter Listeneintrag vollständig abdeckt und die Liste für diese Familie damit als Auswahl aufhebt.
creationell_captcha_hardening_mapped_ipv4_cidr()  : string
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_is_catch_all_pattern()  : bool
Prüft, ob ein `bypass_ua_allow`-Muster jeden Browser-Kennzeichner trifft.
creationell_captcha_hardening_matches_every_user_agent()  : bool
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_scan()  : array<int, array{id: string, liste: string, eintrag: string, wirkung: string, tab: string, meldung: string}>
Durchsucht einen Satz Einstellungen nach gefährlicher Bestandskonfiguration.
creationell_captcha_hardening_findings()  : array<int, array{id: string, liste: string, eintrag: string, wirkung: string, tab: string, meldung: string}>
Führt den Scan gegen die aktuell gespeicherten Einstellungen aus.
creationell_captcha_hardening_fingerprint()  : string
Kurzkennung des aktuellen Fundbildes.
creationell_captcha_hardening_dismiss_redirect_target()  : string
Baut das Ziel des Redirects nach dem Ausblenden-Klick: dieselbe Seite ohne den Ausblenden-Parameter und ohne die Nonce.
creationell_captcha_hardening_handle_dismiss()  : void
Nimmt den Ausblenden-Klick entgegen.
creationell_captcha_hardening_render_notice()  : void
Zeigt die gefundene Bestandskonfiguration als Admin-Hinweis.
creationell_captcha_resolve_widget_locale()  : string|null
Resolves the active WordPress locale to a vendored ALTCHA locale code.
creationell_captcha_get_default_settings()  : array<string, mixed>
Default plugin settings.
creationell_captcha_get_settings()  : array<string, mixed>
Current plugin settings, merged over the defaults.
creationell_captcha_invalidate_settings_cache()  : void
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_is_disabled()  : bool
Whether the captcha is globally disabled via the wp-config constant.
creationell_captcha_sodium_available()  : bool
Whether ext-sodium (required for Argon2id) is available.
creationell_captcha_generate_secrets()  : array{signature: string, key_signature: string}
Generates both HMAC secrets and persists them (non-autoloaded).
creationell_captcha_get_secret()  : string
Returns a stored HMAC secret, generating + persisting it on first use.
creationell_captcha_get_hmac_secret()  : string
The HMAC signature secret (signs each challenge).
creationell_captcha_get_hmac_key_secret()  : string
The HMAC key-signature secret (enables the fast verification path).
creationell_captcha_derive_hmac_key()  : string
Derives a purpose-bound HMAC key from the plugin's base signature secret.
creationell_captcha_client_ua()  : string
The request's User-Agent, capped at 256 bytes. MAC input for the under-attack tokens — never rendered, never stored.
creationell_captcha_underattack_pass_binding()  : string
The fingerprint the under-attack pass cookie is bound to (BK-3).
creationell_captcha_underattack_pass_issue()  : string
Mints an under-attack pass token for the current visitor.
creationell_captcha_underattack_pass_check()  : bool
Verifies an under-attack pass token against the CURRENT visitor.
creationell_captcha_underattack_ctx_issue()  : string
Mints a single-use `ctx` suppression token for one interstitial rendering.
creationell_captcha_underattack_ctx_check()  : bool
Verifies a `ctx` suppression token and consumes it.
creationell_captcha_engine()  : Engine
Shared captcha engine instance.
creationell_captcha_log()  : void
Write a message to the debug log when CREATIONELL_CAPTCHA_DEBUG is active.
creationell_captcha_normalize_ip()  : string
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_get_client_ip()  : string
Resolves the client IP address.
creationell_captcha_ip_in_list()  : bool
Whether an IP matches any entry in a list of IPs or CIDR ranges.
creationell_captcha_ip_in_cidr()  : bool
Whether an IP falls within a CIDR range. Supports IPv4 and IPv6.
creationell_captcha_is_valid_ip_or_cidr()  : bool
Whether a string is a valid IP address or CIDR range (IPv4 or IPv6).
creationell_captcha_wildcard_match()  : bool
Whether a subject matches any of the given wildcard patterns (case-insensitive).
creationell_captcha_private_ranges()  : array<string|int, string>
Returns the canonical list of private/loopback CIDR ranges used when the `firewall_trust_private_ranges` toggle is active.
creationell_captcha_trusted_proxies_constant()  : 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_is_trusted_proxy()  : bool
Whether the given IP belongs to a trusted upstream proxy.
creationell_captcha_evaluate_bypass()  : array{reason: string, source: string}|false
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_request_bypassed()  : array{reason: string, source: string}|false
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_validate_action_pattern()  : string|null
Validates a single interceptor-action pattern.
creationell_captcha_validate_cookie_entry()  : string|null
Validates a single bypass-cookie entry of the form `name=value`.
creationell_captcha_anonymize_ip()  : string
Truncates an IP for DSGVO-compliant storage. IPv4 → last octet zeroed, IPv6 → last 80 bits zeroed. Invalid IPs return ''.
creationell_captcha_request_body_fingerprint()  : string
Returns a JSON-encoded fingerprint of $_POST: { field-name: value-byte-length }.
creationell_captcha_block_response()  : void
Sends a fail-closed block response and terminates the request.
creationell_captcha_base64url_encode()  : string
Base64URL encoder (RFC 4648 §5) — strips standard-base64 padding and replaces +/ with -_ so the value is URL-safe.
creationell_captcha_base64url_decode()  : string
Base64URL decoder — accepts unpadded URL-safe input and returns the raw bytes. Returns the empty string on malformed input (no exceptions).
creationell_captcha_ratelimit_current_count()  : int
Reads the current rate-limit counter for an IP without incrementing it.
creationell_captcha_cf7_active()  : bool
Whether the Contact Form 7 integration is active.
creationell_captcha_cf7_register_tag()  : void
Registers the [creationell_captcha] Contact Form 7 form-tag.
creationell_captcha_cf7_tag_handler()  : string
Renders the widget for the [creationell_captcha] form-tag.
creationell_captcha_cf7_auto_inject()  : string
Auto-injects the widget into CF7 forms without a [creationell_captcha] tag.
creationell_captcha_cf7_verify()  : bool
Verifies the captcha on a Contact Form 7 submission.
creationell_captcha_forminator_active()  : bool
Whether the Forminator integration is active.
creationell_captcha_forminator_inject()  : string
Auto-injects the widget before the submit button of a Forminator custom form.
creationell_captcha_forminator_verify()  : array<int, array<string, string>>
Verifies the captcha on a Forminator custom-form submission.
creationell_captcha_woocommerce_active()  : bool
Whether any WooCommerce protection applies right now.
creationell_captcha_wc_uses_block_checkout()  : bool
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_checkout_active()  : bool
Whether the WooCommerce checkout protection is active.
creationell_captcha_wc_checkout_render()  : void
Renders the widget directly before the Place-Order button on the checkout.
creationell_captcha_wc_checkout_verify_cache()  : bool|null
Per-request cache of the checkout's own verify verdict, keyed by the raw `altcha` payload currently in `$_POST`.
creationell_captcha_wc_checkout_verify()  : void
Verifies the captcha during checkout validation.
creationell_captcha_wc_login_active()  : bool
Whether the WooCommerce my-account login protection is active.
creationell_captcha_wc_login_render()  : void
Renders the widget at the bottom of the WooCommerce login form.
creationell_captcha_wc_login_verify()  : mixed
Verifies the captcha on a WooCommerce my-account login submission.
creationell_captcha_wc_registration_active()  : bool
Whether the WooCommerce registration protection is active.
creationell_captcha_wc_registration_render()  : void
Renders the widget at the bottom of the WooCommerce registration form.
creationell_captcha_wc_nonce_verifies()  : bool
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_registration_form_context()  : bool
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_verify()  : mixed
Verifies the captcha during WooCommerce my-account registration.
creationell_captcha_wc_lost_password_active()  : bool
Whether the WooCommerce lost-password render is active.
creationell_captcha_wc_lost_password_render()  : void
Renders the widget inside the WooCommerce lost-password form.
creationell_captcha_wpforms_active()  : bool
Whether the WPForms integration is active.
creationell_captcha_wpforms_inject()  : void
Auto-injects the widget directly before the WPForms submit button.
creationell_captcha_wpforms_verify()  : void
Verifies the captcha on a WPForms submission.
creationell_captcha_interceptor_inject_buffer_start()  : void
Conditionally starts the output buffer on template_redirect priority 0.
creationell_captcha_interceptor_inject_buffer()  : string
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_run_interceptor()  : void
Runs the request interceptor. Hooked on `init` at priority 1 so it fires before any form-processing handler.
creationell_captcha_protect_path()  : void
Registers one or more path patterns to be guarded by the interceptor.
creationell_captcha_activate()  : void
Runs on plugin activation: seeds default options and HMAC secrets.
creationell_captcha_deactivate()  : 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_site()  : void
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_network()  : int
Runs the per-site deactivation work on every site of the network.
creationell_captcha_delete_expired_replay_markers()  : int
Deletes those replay-marker options of the current site whose lifetime has already run out.
creationell_captcha_run_rate_limiter()  : void
Runs the per-IP rate limiter. Hooked on `init` at priority 0; registered after the firewall so the firewall runs first.
creationell_captcha_request_path()  : string
Returns the path portion of the current request, the way WordPress core derives it — NOT the way a URL parser would.
creationell_captcha_request_target()  : string
Der aktuelle Request als relativer Ziel-URI: Pfad aus `creationell_captcha_request_path()`, Query aus derselben Zeichenkette.
creationell_captcha_current_rest_route()  : string
Returns the REST route the current request actually addresses.
creationell_captcha_normalize_rest_route()  : string
Normalises a raw `rest_route` value into the plugin's canonical route form.
creationell_captcha_register_rest_routes()  : void
Registers the public challenge route.
creationell_captcha_rest_challenge()  : WP_REST_Response
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_canonical_params_json()  : string
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_sort_recursive()  : void
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_list_setting_keys()  : array<int, string>
Setting keys whose value is a list (every `textarea` field).
creationell_captcha_export_settings()  : array<string, mixed>
Builds the settings-export payload.
creationell_captcha_import_settings()  : array<string, mixed>|WP_Error
Validates and applies a settings-export payload.
creationell_captcha_reset_settings()  : void
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_load_default_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_admin_tabs()  : array<string, string>
Ordered list of the admin settings tabs.
creationell_captcha_admin_sections()  : array<string, array<string, string>>
Settings sections and the tab each one belongs to.
creationell_captcha_settings_fields()  : array<string, array<string, mixed>>
Field specification for the captcha settings.
creationell_captcha_register_settings()  : void
Registers the plugin setting, the per-tab sections and the fields.
creationell_captcha_sanitize_context()  : string|null
Liest — und setzt optional — den angehefteten Sanitizer-Kontext.
creationell_captcha_with_sanitize_context()  : mixed
Führt $callback aus, während der Sanitizer-Kontext auf $context festgelegt ist.
creationell_captcha_sanitize_settings_option()  : array<string, mixed>
Einstiegspunkt des `sanitize_option_creationell_captcha_settings`-Filters.
creationell_captcha_truncate_setting_text()  : string
Kürzt einen Einstellungswert auf höchstens $max_chars Zeichen und garantiert gültiges UTF-8.
creationell_captcha_truncate_chars()  : string
Kürzt gültiges UTF-8 ohne mbstring auf $max_chars ZEICHEN.
creationell_captcha_truncate_chars_by_bytes()  : string
Dritte und letzte Kürzungsstufe: zählt UTF-8-Startbytes.
creationell_captcha_sanitize_settings()  : array<string, mixed>
Sanitises the settings array before it is stored.
creationell_captcha_sync_event_log_table()  : void
Legt die Ereignis-Log-Tabelle an, sobald das Detail-Log eingeschaltet GESPEICHERT wurde.
creationell_captcha_store_settings()  : void
Der eine Schreibweg des Plugins auf `creationell_captcha_settings`.
creationell_captcha_render_engine_section()  : void
Renders the description shown at the top of the Proof-of-Work-Engine section.
creationell_captcha_render_widget_appearance_section()  : void
Renders the description shown at the top of the widget-appearance section.
creationell_captcha_render_code_challenge_section()  : void
Renders the description shown at the top of the code-challenge section.
creationell_captcha_render_core_forms_section()  : void
Renders the description shown at the top of the core-forms section.
creationell_captcha_render_interceptor_section()  : void
Renders the description shown at the top of the interceptor section.
creationell_captcha_render_form_plugins_section()  : void
Renders the description shown at the top of the form-plugins section.
creationell_captcha_render_proxy_section()  : void
Renders the description shown at the top of the proxy section.
creationell_captcha_render_bypass_section()  : void
Renders the description shown at the top of the bypass section.
creationell_captcha_render_firewall_section()  : void
Renders the description shown at the top of the firewall section.
creationell_captcha_render_ratelimit_section()  : void
Renders the description shown at the top of the rate-limiting section.
creationell_captcha_render_underattack_section()  : void
Renders the description shown at the top of the under-attack section.
creationell_captcha_render_underattack_appearance_section()  : void
Renders the description shown at the top of the under-attack appearance section.
creationell_captcha_render_analytics_section()  : void
Renders the description shown at the top of the analytics section.
creationell_captcha_render_email_section()  : void
Renders the description shown at the top of the email-protection section.
creationell_captcha_render_field()  : void
Renders a single settings field.
creationell_captcha_field_label()  : string
Liefert das Label eines Einstellungsfeldes (Fallback: der Schlüssel selbst).
creationell_captcha_field_spec()  : array<string, array<string, mixed>>
Request-lokal gehaltene Feldspezifikation für die beiden Label-Helfer.
creationell_captcha_field_option_label()  : string
Liefert das Options-Label eines `select`-Feldes (Fallback: der Rohwert).
creationell_captcha_tools_redirect()  : never
Stores a one-shot admin notice and redirects back to the Werkzeuge page.
creationell_captcha_tools_guard()  : void
Guards a tools action: requires manage_options and a valid nonce.
creationell_captcha_handle_export_settings()  : void
Streams the current settings as a JSON download.
creationell_captcha_handle_import_settings()  : void
Handles the settings-import upload.
creationell_captcha_collect_settings_error_messages()  : array<int, string>
Collects the plugin's queued settings-error messages, de-duplicated.
creationell_captcha_handle_reset_settings()  : void
Handles the full factory reset.
creationell_captcha_handle_load_defaults()  : void
Handles "load defaults" (keeps the lists).
creationell_captcha_handle_cloudflare_refresh()  : void
Triggers a manual Cloudflare-range refresh from the Werkzeuge page.
creationell_captcha_handle_cloudflare_clear()  : void
Empties the cached Cloudflare-range option from the Werkzeuge page.
creationell_captcha_register_tools_page()  : void
Registers the "Werkzeuge" submenu page under the CreaCaptcha menu.
creationell_captcha_render_tools_notice()  : void
Renders the one-shot admin notice left behind by a tools action.
creationell_captcha_render_tools_page()  : void
Renders the "Werkzeuge" page.
creationell_captcha_render_cloudflare_status()  : void
Renders the Cloudflare-cache status block inside the Werkzeuge tool card.
creationell_captcha_run_under_attack()  : 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_maybe_upgrade()  : void
Runs schema migrations when the stored version differs from the running one.
creationell_captcha_migrate_widget_mode()  : void
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_store_migrated_settings()  : void
Writes a migrated settings array back — through the sanitiser, with the write context pinned.
creationell_captcha_register_assets()  : void
Registers the widget script and — for Argon2id — its worker registration.
creationell_captcha_safe_inline_css()  : string
Macht eine gespeicherte CSS-Zeichenkette sicher für die Ausgabe in einem `<style>`-Element.
creationell_captcha_build_widget_markup()  : string
Builds the ALTCHA widget markup as a plain string. Enqueues the widget script as a side effect.
creationell_captcha_render_widget()  : void
Renders the ALTCHA widget markup and enqueues its assets.
creationell_captcha_verify_payload()  : bool
Verifies a raw base64 ALTCHA payload string.
creationell_captcha_verify_request()  : bool
Reads and verifies the ALTCHA payload from the current POST request.
creationell_captcha_widget()  : void
Public template tag — renders the ALTCHA widget.
creationell_captcha_get_widget_markup()  : string
Returns the ALTCHA widget markup as a string.
creationell_captcha_widget_shortcode()  : string
Shortcode handler for [creationell_captcha].
creationell_captcha_uninstall_site()  : void
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_user_meta()  : void
Removes the plugin's per-user data.

Constants

CREATIONELL_CAPTCHA_EXPORT_MAX_ROWS

The default upper bound on rows in a single CSV export.

public mixed CREATIONELL_CAPTCHA_EXPORT_MAX_ROWS = 50000

CREATIONELL_CAPTCHA_EXPORT_SCHEMA

Schema version of the settings-export format.

public mixed CREATIONELL_CAPTCHA_EXPORT_SCHEMA = 1

CREATIONELL_CAPTCHA_HARDENING_DISMISS_ARG

Query-Parameter des Ausblenden-Links.

public mixed CREATIONELL_CAPTCHA_HARDENING_DISMISS_ARG = 'creationell_captcha_hardening_dismiss'

CREATIONELL_CAPTCHA_HARDENING_DISMISS_META

User-Meta-Schlüssel, unter dem ein Benutzer den Hinweis wegklickt.

public mixed CREATIONELL_CAPTCHA_HARDENING_DISMISS_META = 'creationell_captcha_hardening_dismissed'

CREATIONELL_CAPTCHA_LOCALE_MAP

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.

CREATIONELL_CAPTCHA_PURPOSE_UA_CTX

Purpose label of the under-attack code-challenge suppression token (`ctx`).

public mixed CREATIONELL_CAPTCHA_PURPOSE_UA_CTX = 'underattack-ctx'

CREATIONELL_CAPTCHA_PURPOSE_UA_PASS

Purpose label of the under-attack pass-cookie key.

public mixed CREATIONELL_CAPTCHA_PURPOSE_UA_PASS = 'underattack-pass'

CREATIONELL_CAPTCHA_SANITIZE_FORM

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.

CREATIONELL_CAPTCHA_SANITIZE_PROGRAMMATIC

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).

CREATIONELL_CAPTCHA_SANITIZE_RESET

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.

Tags
since
1.1.0

CREATIONELL_CAPTCHA_UA_CTX_TTL

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

Functions

creationell_captcha_register_admin_menu()

Registers the top-level "CreaCaptcha" admin menu entry.

creationell_captcha_register_admin_menu() : void

creationell_captcha_render_settings_page()

Renders the tabbed settings page.

creationell_captcha_render_settings_page() : void

All tabs share one

and one submit button — the tab panels are only a display split, so saving always persists every field at once. PHP marks one tab active server-side; admin.js switches tabs client-side; the

creationell_captcha_render_import_warning()

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()).

creationell_captcha_render_trust_notice()

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.

creationell_captcha_tabbed_page_hooks()

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.

Parameters
$add : mixed = null

Hook suffix to record; ignored unless a non-empty string.

Return values
array<int, string> —

All recorded hook suffixes.

creationell_captcha_active_tab()

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.

Parameters
$tabs : array<string, string>

Tab registry (id => label).

Return values
string —

Active tab id.

creationell_captcha_render_nav_tabs()

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

Parameters
$tabs : array<string, string>

Tab registry (id => label).

$active_tab : string

Active tab id.

$base_url : string

Page URL the tab links point at.

creationell_captcha_csv_cell()

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".

Parameters
$value : string

Raw cell value.

Return values
string

creationell_captcha_export_events()

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).

creationell_captcha_register_analytics_page()

Registers the "Statistik" submenu page under the CreaCaptcha menu.

creationell_captcha_register_analytics_page() : void

creationell_captcha_analytics_labels()

Human-readable labels for the seven event types.

creationell_captcha_analytics_labels() : array<string, string>
Return values
array<string, string>

creationell_captcha_analytics_groups()

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.

Return values
array<int, array{title: string, description: string, types: array}>

creationell_captcha_analytics_tabs()

The three tabs of the analytics page.

creationell_captcha_analytics_tabs() : array<string, string>
Return values
array<string, string> —

Tab id => visible label.

creationell_captcha_sum_recent_days()

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.

Parameters
$daily : array<string, array<string, int>>

Daily counters keyed by 'Y-m-d'.

$days : int

Number of day buckets to sum.

Return values
array<string, int> —

Event type => sum.

creationell_captcha_sum_recent_hours()

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.

Parameters
$hourly : array<string, array<string, int>>

Hourly counters keyed by 'Y-m-d H'.

$hours : int

Number of hour buckets to sum.

Return values
array<string, int> —

Event type => sum.

creationell_captcha_render_analytics_page()

Renders the tabbed analytics dashboard page.

creationell_captcha_render_analytics_page() : void

creationell_captcha_render_analytics_overview()

Renders the "Übersicht" tab: four 24-hour KPI tiles and one four-window comparison table per thematic group.

creationell_captcha_render_analytics_overview() : void

creationell_captcha_render_analytics_history()

Renders the "Verlauf" tab: the day-by-day table for the last 30 days.

creationell_captcha_render_analytics_history() : void

creationell_captcha_events_query_args()

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.

Return values
array{search: string, event_type: string, date_from: string, date_to: string}

creationell_captcha_render_analytics_events()

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

creationell_captcha_render_event_modal()

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.

creationell_captcha_render_events_toolbar()

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
Parameters
$filter : array{search: string, event_type: string, date_from: string, date_to: string}

Active filter.

creationell_captcha_render_events_pagination()

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
Parameters
$filter : array{search: string, event_type: string, date_from: string, date_to: string}

Active filter.

$paged : int

Current page (1-based).

$total_pages : int

Total page count.

creationell_captcha_analytics()

Returns the shared analytics recorder instance.

creationell_captcha_analytics() : Analytics
Return values
Analytics

creationell_captcha_record_event()

Records a security event of the given type.

creationell_captcha_record_event(string $type[, array<string, mixed> $context = [] ]) : void
Parameters
$type : string

The event type.

$context : array<string, mixed> = []

Optional caller-supplied context.

creationell_captcha_enqueue_admin_assets()

Enqueues admin styles and scripts on the CreaCaptcha admin pages.

creationell_captcha_enqueue_admin_assets(string $hook_suffix) : void
Parameters
$hook_suffix : string

Current admin page hook suffix.

creationell_captcha_cloudflare_snapshot()

Returns the bundled Cloudflare snapshot.

creationell_captcha_cloudflare_snapshot() : array{v4: string[], v6: string[], updated_at: string}
Return values
array{v4: string[], v6: string[], updated_at: string}

creationell_captcha_cloudflare_ranges()

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.

Return values
array<string|int, string> —

IPv4 and IPv6 CIDR ranges, merged.

creationell_captcha_refresh_cloudflare_ips_now()

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}
Return values
array{ok: bool, v4: int, v6: int, fetched_at: int|null, error: string|null}

creationell_captcha_clear_cloudflare_cache()

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
Return values
bool

creationell_captcha_fetch_cloudflare_list()

Fetches one Cloudflare endpoint and returns the valid CIDR entries.

creationell_captcha_fetch_cloudflare_list(string $url) : array<string|int, string>
Parameters
$url : string
Return values
array<string|int, string>

creationell_captcha_sync_cloudflare_cron()

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.

creationell_captcha_code_challenge_font_usable()

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.

Return values
bool

creationell_captcha_render_code_image()

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.

Parameters
$code : string

The code to render (4–8 chars expected; longer is trimmed implicitly by the width budget).

Return values
string

creationell_captcha_should_issue_code_challenge()

Decides whether the current /challenge request should attach a code-challenge instruction. Returns true iff:

creationell_captcha_should_issue_code_challenge() : bool
  1. the master toggle is on, AND
  2. PHP-GD is available, AND
  3. at least one of the three trigger conditions matches (under-attack, ratelimit threshold, watch-list).
Return values
bool

creationell_captcha_code_charset()

Returns the active charset string for the configured option.

creationell_captcha_code_charset(string $charset_key) : string
Parameters
$charset_key : string

One of: digits / alphanumeric / alphanumeric-no-confusing.

Return values
string

creationell_captcha_generate_code()

Generates a fresh random code from the configured charset.

creationell_captcha_generate_code() : string
Return values
string

creationell_captcha_code_token_issue()

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.

Parameters
$code : string

The expected code (already from generator).

$expiry_seconds : int

Seconds until the token expires.

Return values
string

creationell_captcha_code_token_verify()

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
Parameters
$token : string

The 32-hex-char ID from code_token_issue.

$consume : bool = false

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.

Return values
string|null

creationell_captcha_code_fail_key()

Transient name of the per-token failed-attempt counter (CM-3).

creationell_captcha_code_fail_key(string $token) : string
Parameters
$token : string

The 32-hex-char ID from code_token_issue.

Return values
string

creationell_captcha_code_token_fail()

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.

Parameters
$token : string

The 32-hex-char ID from code_token_issue.

creationell_captcha_rest_code_image()

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.

Parameters
$request : WP_REST_Request

The REST request. Required query parameter t (opaque server-issued token).

Return values
WP_REST_Response —

200 with image/png body on success; 410 when the token is unknown/expired or PHP-GD is unavailable.

creationell_captcha_rest_code_verify()

Handles POST /code-verify. Two body shapes:

creationell_captcha_rest_code_verify(WP_REST_Request $request) : WP_REST_Response
  • Code-Challenge mode: { "code": "", "payload": "" } The widget rendered a code-image because the /challenge response embedded data.ccode. Token lookup → case-insensitive match → single-use consume → "code stage passed" marker for this challenge.

    • Plain server-verify mode: { "payload": "" } (no code) ALTCHA's widget posts here unconditionally whenever verifyUrl is set (see widget.js logic: 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.

Parameters
$request : WP_REST_Request

The REST request with JSON body {payload: string, code?: string}.

Return values
WP_REST_Response —

200 on success ({payload, verified: true}); 400 on malformed body; 401 on wrong code; 410 on missing/expired/unsigned payload or token.

creationell_captcha_register_code_challenge_routes()

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

creationell_captcha_register_email_obfuscation()

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

creationell_captcha_register_email_buffer()

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.

Parameters
$obfuscator : EmailObfuscator

The obfuscator.

creationell_captcha_email_buffer_filter()

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.

Parameters
$html : string

Der gepufferte Antwort-Body.

$obfuscator : EmailObfuscator

The obfuscator.

Return values
string —

Umgeschriebener oder unveränderter Body.

creationell_captcha_response_is_html()

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.

Return values
bool

creationell_captcha_run_firewall()

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

creationell_captcha_comments_active()

Whether comment protection applies to the current request.

creationell_captcha_comments_active() : bool
Return values
bool

creationell_captcha_comments_render()

Injects the widget just above the comment form submit button.

creationell_captcha_comments_render(string $submit_field) : string
Parameters
$submit_field : string

The submit button field HTML.

Return values
string

creationell_captcha_comments_verify()

Verifies the captcha before a comment is accepted.

creationell_captcha_comments_verify(array<string, mixed> $commentdata) : array<string, mixed>
Parameters
$commentdata : array<string, mixed>

Comment data.

Return values
array<string, mixed>

creationell_captcha_login_enabled()

Whether login protection is enabled.

creationell_captcha_login_enabled() : bool
Return values
bool

creationell_captcha_login_render()

Renders the widget inside the login form.

creationell_captcha_login_render() : void

creationell_captcha_login_form_middle()

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).

Parameters
$content : string

Existing middle-of-form markup.

Return values
string

creationell_captcha_login_is_interactive()

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).

Return values
bool

creationell_captcha_login_verify()

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
Parameters
$user : WP_User|WP_Error|null

Authenticated user or error.

$username : string

Submitted username.

$password : string

Submitted password.

Return values
WP_User|WP_Error|null

creationell_captcha_password_reset_enabled()

Whether password-reset protection is enabled.

creationell_captcha_password_reset_enabled() : bool
Return values
bool

creationell_captcha_password_reset_render()

Renders the widget inside the lost-password form.

creationell_captcha_password_reset_render() : void

creationell_captcha_password_reset_exempt_reason()

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.

Return values
string|null —

Exemption reason, or null for a genuine form submission.

creationell_captcha_password_reset_exempt_label()

Human-readable label for an exemption reason, for the event log.

creationell_captcha_password_reset_exempt_label(string $reason) : string
Parameters
$reason : string

Reason key from creationell_captcha_password_reset_exempt_reason().

Return values
string

creationell_captcha_password_reset_verify()

Verifies the captcha during a password-reset request.

creationell_captcha_password_reset_verify(WP_Error $errors) : void
Parameters
$errors : WP_Error

Password-reset errors (passed by WordPress >= 5.4).

creationell_captcha_registration_enabled()

Whether registration protection is enabled.

creationell_captcha_registration_enabled() : bool
Return values
bool

creationell_captcha_registration_render()

Renders the widget inside the registration form.

creationell_captcha_registration_render() : void

creationell_captcha_registration_verify()

Verifies the captcha during registration.

creationell_captcha_registration_verify(WP_Error $errors, string $sanitized_user_login, string $user_email) : WP_Error
Parameters
$errors : WP_Error

Registration errors.

$sanitized_user_login : string

Submitted user login.

$user_email : string

Submitted user email.

Return values
WP_Error

creationell_captcha_hardening_covered_family()

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):

  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.
  2. 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.

Parameters
$entry : string

Eintrag aus der gespeicherten Liste (bereits getrimmt).

Return values
string —

'IPv4', 'IPv6' oder '' — Letzteres heißt: deckt keine Familie vollständig ab, also kein Fund.

creationell_captcha_hardening_mapped_ipv4_cidr()

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

WARUM DAS EINE EIGENE PRÜFUNG IST

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.

WAS DER EINTRAG BEDEUTET — GEMESSEN, NICHT VERMUTET

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).

Parameters
$entry : string

Eintrag aus der gespeicherten Liste (bereits getrimmt).

Return values
string —

Gewöhnliche IPv4-Schreibweise des Bereichs, oder '' wenn der Eintrag kein IPv4-Bereich in mapped Notation ist.

creationell_captcha_hardening_is_catch_all_pattern()

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.

GRENZE DIESER PRÜFUNG (Befund B-M2)

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.

Parameters
$pattern : string

Muster aus der gespeicherten Liste.

Return values
bool

creationell_captcha_hardening_matches_every_user_agent()

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.

DAS KRITERIUM IST BEWUSST „ALLE SONDEN" UND NICHT „EINE SONDE"

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.

Parameters
$pattern : string

Muster aus der gespeicherten Liste.

Return values
bool

creationell_captcha_hardening_scan()

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.

Parameters
$settings : array<string, mixed>

Gespeicherte Plugin-Einstellungen.

$gd_available : bool|null = null

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.

Return values
array<int, array{id: string, liste: string, eintrag: string, wirkung: string, tab: string, meldung: string}>

creationell_captcha_hardening_findings()

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}>
Return values
array<int, array{id: string, liste: string, eintrag: string, wirkung: string, tab: string, meldung: string}>

creationell_captcha_hardening_fingerprint()

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).

Parameters
$findings : array<int, array<string, string>>

Fundliste aus dem Scan.

Return values
string —

16 Hex-Zeichen, oder '' wenn es nichts zu melden gibt.

creationell_captcha_hardening_dismiss_redirect_target()

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.

Tags
since
1.1.0
Return values
string —

Relativer Ziel-URI für wp_safe_redirect().

creationell_captcha_hardening_handle_dismiss()

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.

creationell_captcha_hardening_render_notice()

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.

creationell_captcha_resolve_widget_locale()

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.

Tags
since
0.27.0
Return values
string|null —

Vendored ALTCHA locale code or null.

creationell_captcha_get_default_settings()

Default plugin settings.

creationell_captcha_get_default_settings() : array<string, mixed>
Return values
array<string, mixed>

creationell_captcha_get_settings()

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.

Parameters
$force_refresh : bool = false

Re-read from the DB even if a cached copy exists. Used by the invalidation hook.

Return values
array<string, mixed>

creationell_captcha_invalidate_settings_cache()

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

creationell_captcha_is_disabled()

Whether the captcha is globally disabled via the wp-config constant.

creationell_captcha_is_disabled() : bool
Return values
bool

creationell_captcha_sodium_available()

Whether ext-sodium (required for Argon2id) is available.

creationell_captcha_sodium_available() : bool
Return values
bool

creationell_captcha_generate_secrets()

Generates both HMAC secrets and persists them (non-autoloaded).

creationell_captcha_generate_secrets() : array{signature: string, key_signature: string}
Return values
array{signature: string, key_signature: string}

creationell_captcha_get_secret()

Returns a stored HMAC secret, generating + persisting it on first use.

creationell_captcha_get_secret(string $which) : string
Parameters
$which : string

Either 'signature' or 'key_signature'.

Return values
string

creationell_captcha_get_hmac_secret()

The HMAC signature secret (signs each challenge).

creationell_captcha_get_hmac_secret() : string

A wp-config constant takes precedence over the stored option.

Return values
string

creationell_captcha_get_hmac_key_secret()

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.

Return values
string

creationell_captcha_derive_hmac_key()

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.

Parameters
$purpose : string

Stable purpose label, e.g. underattack-pass.

Tags
since
1.1.0
Return values
string —

64-char hex key, or '' when no base secret is available (callers must fail closed on '').

creationell_captcha_client_ua()

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
Tags
since
1.1.0
Return values
string

creationell_captcha_underattack_pass_binding()

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.

  • the client NETWORK (IPv4 /24, IPv6 /48 — the same reduction the analytics anonymiser applies), resolved through 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;
  • the User-Agent.

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.

Tags
since
1.1.0
Return values
string —

Binding string; '' disables the binding entirely.

creationell_captcha_underattack_pass_issue()

Mints an under-attack pass token for the current visitor.

creationell_captcha_underattack_pass_issue(int $expiry) : string
Parameters
$expiry : int

Absolute Unix timestamp the pass runs out at.

Tags
since
1.1.0
Return values
string —

<expiry>.<mac> token, or '' when no key could be derived.

creationell_captcha_underattack_pass_check()

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.

Parameters
$cookie : string

Raw cookie value.

Tags
since
1.1.0
Return values
bool

creationell_captcha_underattack_ctx_issue()

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:

  • REPLAY is closed. The nonce is burned on first use, so one token buys at most one suppressed challenge (1.0.2: unlimited inside its bucket pair).
  • The accepted WINDOW shrinks from ~10 minutes to 120 seconds.
  • ACQUISITION stays open. One GET of a 503 page still yields one fresh token, i.e. one code-stage-free challenge. Closing that needs the issued challenge itself to be marked under-attack-only and checked on redemption inside Engine::verify() — outside this file (R1 in the task report).
  • HANDING A FRESH TOKEN ON stays open as well. The MAC covers the User-Agent, but the User-Agent is a value the RECIPIENT sends himself: whoever passes the token to a bot passes the UA string along with it. The binding costs an attacker one header, no more. It is kept because it is free and it does stop a token that leaked WITHOUT its UA (log excerpt, referrer, a URL shared out of band). It is not a transfer barrier, and no comment on this token may claim that it is — W5 was exactly that kind of claim, made about exactly this token.

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).

Tags
since
1.1.0
Return values
string —

<nonce>.<expiry>.<mac>, or '' when no key could be derived.

creationell_captcha_underattack_ctx_check()

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.

Parameters
$token : string

Raw ctx query value.

Tags
since
1.1.0
Return values
bool

creationell_captcha_engine()

Shared captcha engine instance.

creationell_captcha_engine() : Engine
Return values
Engine

creationell_captcha_log()

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.

Parameters
$message : string

Message to log.

creationell_captcha_normalize_ip()

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

WHY (audit module 27, finding I2)

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;
  • so 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.

Parameters
$ip : string

Candidate address.

Tags
since
1.1.0
Return values
string —

Plain IPv4 spelling for mapped addresses, else the input.

creationell_captcha_get_client_ip()

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:

  • the rate limiter counts the whole site into one bucket and locks everyone out at the threshold;
  • firewall_ip_block and code_challenge_watchlist hit all or nothing;
  • and if the proxy address happens to sit in 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.

Return values
string

creationell_captcha_ip_in_list()

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().

Parameters
$ip : string

The client IP.

$list : mixed

A list of IPs / CIDR ranges (non-arrays are ignored).

Return values
bool

creationell_captcha_ip_in_cidr()

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.

Parameters
$ip : string

The client IP.

$cidr : string

A CIDR range, e.g. "203.0.113.0/24".

Return values
bool

creationell_captcha_is_valid_ip_or_cidr()

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.

Parameters
$entry : string

The candidate string.

Return values
bool

creationell_captcha_wildcard_match()

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.

WHERE $allow_catch_all STOPS — READ THIS BEFORE TRUSTING IT

The 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.

Parameters
$subject : string

The string to test.

$patterns : mixed

A list of patterns; non-arrays return false.

$empty_subject_matches : bool = false

What an empty subject means for this call site. Default false (previous behaviour).

$allow_catch_all : bool = true

Whether a bare * pattern is honoured. Default true (previous behaviour).

Return values
bool

creationell_captcha_private_ranges()

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>
Return values
array<string|int, string>

creationell_captcha_trusted_proxies_constant()

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>
Return values
array<string|int, string>

creationell_captcha_is_trusted_proxy()

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:

  1. firewall_trusted_proxies (the explicit textarea list)
  2. CREATIONELL_CAPTCHA_TRUSTED_PROXIES (wp-config constant)
  3. firewall_trust_private_ranges (when on): the private/loopback ranges
  4. firewall_trust_cloudflare (when on): the cached/bundled CF ranges
Parameters
$ip : string

A validated client IP address.

Return values
bool

creationell_captcha_evaluate_bypass()

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:

  1. firewall_ip_allow vs $ip
  2. bypass_ua_allow vs $ua
  3. bypass_cookies vs $cookies (strict name=value)
Parameters
$ip : string|null

Client IP, or null to skip the IP check.

$ua : string|null

User-Agent, or null to skip the UA check.

$cookies : array<string, string>

Cookie map (name => value).

Return values
array{reason: string, source: string}|false

creationell_captcha_request_bypassed()

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.

Return values
array{reason: string, source: string}|false

creationell_captcha_validate_action_pattern()

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.

Parameters
$entry : string

Raw entry (already trimmed by the caller).

Return values
string|null —

Normalised entry, or null if invalid.

creationell_captcha_validate_cookie_entry()

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.

Parameters
$entry : string

Raw entry (already trimmed by the caller).

Return values
string|null —

Normalised name=value entry, or null if invalid.

creationell_captcha_anonymize_ip()

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.

Parameters
$ip : string

A validated client IP address.

Return values
string

creationell_captcha_request_body_fingerprint()

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.

Return values
string

creationell_captcha_block_response()

Sends a fail-closed block response and terminates the request.

creationell_captcha_block_response(int $status, string $message[, int $retry_after = 0 ]) : void
Parameters
$status : int

HTTP status code (403 firewall, 429 rate limit).

$message : string

The message shown to the client.

$retry_after : int = 0

Optional Retry-After value in seconds.

creationell_captcha_base64url_encode()

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
Parameters
$bytes : string

Raw bytes to encode.

Return values
string

creationell_captcha_base64url_decode()

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
Parameters
$encoded : string

URL-safe base64 string.

Return values
string

creationell_captcha_ratelimit_current_count()

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.

Parameters
$ip : string

Client IP (call creationell_captcha_get_client_ip()).

Return values
int

creationell_captcha_cf7_active()

Whether the Contact Form 7 integration is active.

creationell_captcha_cf7_active() : bool
Return values
bool

creationell_captcha_cf7_register_tag()

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.

creationell_captcha_cf7_tag_handler()

Renders the widget for the [creationell_captcha] form-tag.

creationell_captcha_cf7_tag_handler() : string
Return values
string

creationell_captcha_cf7_auto_inject()

Auto-injects the widget into CF7 forms without a [creationell_captcha] tag.

creationell_captcha_cf7_auto_inject(string $elements) : string
Parameters
$elements : string

The form's inner HTML.

Return values
string

creationell_captcha_cf7_verify()

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.

Parameters
$spam : mixed

Whether CF7 already classified the submission as spam.

$submission : mixed

The WPCF7_Submission object.

Return values
bool

creationell_captcha_forminator_active()

Whether the Forminator integration is active.

creationell_captcha_forminator_active() : bool
Return values
bool

creationell_captcha_forminator_inject()

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.

Parameters
$html : mixed

The submit-section HTML.

$form_id : mixed

The form's post ID.

Return values
string

creationell_captcha_forminator_verify()

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.

Parameters
$errors : mixed

The current array of submission errors.

Return values
array<int, array<string, string>>

creationell_captcha_woocommerce_active()

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.

Return values
bool

creationell_captcha_wc_uses_block_checkout()

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).

Return values
bool

creationell_captcha_wc_checkout_active()

Whether the WooCommerce checkout protection is active.

creationell_captcha_wc_checkout_active() : bool
Return values
bool

creationell_captcha_wc_checkout_render()

Renders the widget directly before the Place-Order button on the checkout.

creationell_captcha_wc_checkout_render() : void

creationell_captcha_wc_checkout_verify_cache()

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.

Parameters
$set : bool|null = null

Pass a bool to record the verdict for the current raw payload; omit (null, default) to just read a previously recorded one.

Return values
bool|null —

The cached verdict for the current raw payload, or null if none has been recorded yet this request.

creationell_captcha_wc_checkout_verify()

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.

Parameters
$data : array<string, mixed>

Posted checkout data (unused).

$errors : mixed

The checkout WP_Error (passed by reference of the object).

creationell_captcha_wc_login_active()

Whether the WooCommerce my-account login protection is active.

creationell_captcha_wc_login_active() : bool
Return values
bool

creationell_captcha_wc_login_render()

Renders the widget at the bottom of the WooCommerce login form.

creationell_captcha_wc_login_render() : void

creationell_captcha_wc_login_verify()

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).

Parameters
$validation_error : mixed

The current validation error (WP_Error|null|false).

$username : string

Submitted username (unused).

$password : string

Submitted password (unused).

creationell_captcha_wc_registration_active()

Whether the WooCommerce registration protection is active.

creationell_captcha_wc_registration_active() : bool
Return values
bool

creationell_captcha_wc_registration_render()

Renders the widget at the bottom of the WooCommerce registration form.

creationell_captcha_wc_registration_render() : void

creationell_captcha_wc_nonce_verifies()

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.

Parameters
$source : array<string, mixed>

$_POST or $_REQUEST.

$dedicated_field : string

The form's own nonce field name.

$action : string

The nonce action to verify against.

Return values
bool

creationell_captcha_wc_registration_form_context()

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):

  • Standalone My-Account registration form: 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().
  • Classic checkout with "Create an account?": 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.

Return values
bool

creationell_captcha_wc_registration_verify()

Verifies the captcha during WooCommerce my-account registration.

creationell_captcha_wc_registration_verify(mixed $errors, string $username, string $email) : mixed
Parameters
$errors : mixed

The current WP_Error carrier from WooCommerce.

$username : string

Submitted username (unused).

$email : string

Submitted email (unused).

creationell_captcha_wc_lost_password_active()

Whether the WooCommerce lost-password render is active.

creationell_captcha_wc_lost_password_active() : bool
Return values
bool

creationell_captcha_wc_lost_password_render()

Renders the widget inside the WooCommerce lost-password form.

creationell_captcha_wc_lost_password_render() : void

creationell_captcha_wpforms_active()

Whether the WPForms integration is active.

creationell_captcha_wpforms_active() : bool
Return values
bool

creationell_captcha_wpforms_inject()

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 element, so the hidden altcha input that the widget emits is part of the WPForms submission.

Parameters
$form_data : array<string, mixed>

WPForms form configuration.

$form : mixed = null

WPForms form post (unused, optional — der Hook wpforms_display_submit_before liefert nur $form_data).

creationell_captcha_wpforms_verify()

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.

Parameters
$fields : array<int, mixed>

Sanitized field values (unused).

$entry : array<string, mixed>

Raw $_POST['wpforms'] (unused).

$form_data : array<string, mixed>

Form configuration.

creationell_captcha_interceptor_inject_buffer_start()

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.

creationell_captcha_interceptor_inject_buffer()

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
Parameters
$html : string

Full page HTML.

Return values
string

creationell_captcha_run_interceptor()

Runs the request interceptor. Hooked on `init` at priority 1 so it fires before any form-processing handler.

creationell_captcha_run_interceptor() : void

creationell_captcha_protect_path()

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.

Parameters
$patterns : string|array<int, string>

A path pattern or list of patterns.

creationell_captcha_activate()

Runs on plugin activation: seeds default options and HMAC secrets.

creationell_captcha_activate() : void

creationell_captcha_deactivate()

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.

Parameters
$network_deactivating : bool = false

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.

creationell_captcha_deactivate_site()

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.

creationell_captcha_deactivate_network()

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.

Return values
int —

Number of sites processed.

creationell_captcha_delete_expired_replay_markers()

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.

Return values
int —

Number of option rows removed.

creationell_captcha_run_rate_limiter()

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

creationell_captcha_request_path()

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.

WHY THIS EXISTS (audit module 27, finding C1)

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.

WHAT THIS DOES

  1. Split off query and fragment BEFORE anything is decoded, so a percent-encoded ? inside the path cannot smuggle a query string in.
  2. Strip scheme and authority if the target arrived in absolute form (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.
  3. Collapse leading slashes to exactly one — the same reduction core's 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.
Tags
since
1.1.0
Return values
string —

Request path with exactly one leading slash, still encoded.

creationell_captcha_request_target()

Der aktuelle Request als relativer Ziel-URI: Pfad aus `creationell_captcha_request_path()`, Query aus derselben Zeichenkette.

creationell_captcha_request_target() : string

WARUM DAS EINE EIGENE FUNKTION IST (Nachlese Modul 27, Strang N1; Umfang erweitert im Re-Review und in der Nachlese N6)

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.

Tags
since
1.1.0
Return values
string —

Relativer Ziel-URI (führender Schrägstrich, genau einer).

creationell_captcha_current_rest_route()

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.

Tags
since
1.1.0
Return values
string —

Normalised REST route, or '' when this is not a REST request.

creationell_captcha_normalize_rest_route()

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.

Parameters
$raw : string

Raw rest_route value as core would see it.

Tags
since
1.1.0
Return values
string —

Route with a leading slash and without a trailing one, or ''.

creationell_captcha_register_rest_routes()

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.

creationell_captcha_rest_challenge()

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
Parameters
$request : WP_REST_Request

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().

Return values
WP_REST_Response —

JSON challenge envelope with algorithm, challenge, salt, signature, parameters and optional codeChallenge.image URL.

creationell_captcha_canonical_params_json()

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.

Parameters
$params : array<string, mixed>

Parameter array from create_challenge().

Return values
string

creationell_captcha_canonical_sort_recursive()

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
Parameters
$data : array<string|int, mixed>

creationell_captcha_list_setting_keys()

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.

Return values
array<int, string>

creationell_captcha_export_settings()

Builds the settings-export payload.

creationell_captcha_export_settings() : array<string, mixed>

The HMAC secrets are deliberately excluded — they must never leave the site.

Return values
array<string, mixed>

creationell_captcha_import_settings()

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.

Parameters
$payload : array<string, mixed>

Decoded export payload.

Return values
array<string, mixed>|WP_Error —

On success: { imported, version_notice, source_notice }.

creationell_captcha_reset_settings()

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

creationell_captcha_load_default_settings()

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.

creationell_captcha_admin_tabs()

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.

Return values
array<string, string> —

Tab id => visible label.

creationell_captcha_admin_sections()

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.

Return values
array<string, array<string, string>> —

Section id => { tab, title, callback }.

creationell_captcha_settings_fields()

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().

Return values
array<string, array<string, mixed>>

creationell_captcha_register_settings()

Registers the plugin setting, the per-tab sections and the fields.

creationell_captcha_register_settings() : void

creationell_captcha_sanitize_context()

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).

Parameters
$set : string|false|null = false

FALSE liest nur; ein String oder NULL setzt.

Return values
string|null —

Aktuell angehefteter Kontext oder NULL.

creationell_captcha_with_sanitize_context()

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.

Parameters
$context : string

Einer der CREATIONELL_CAPTCHA_SANITIZE_*-Werte.

$callback : callable

Auszuführender Schreibvorgang.

Return values
mixed —

Rückgabewert von $callback.

creationell_captcha_sanitize_settings_option()

Einstiegspunkt des `sanitize_option_creationell_captcha_settings`-Filters.

creationell_captcha_sanitize_settings_option(mixed $input) : array<string, mixed>
Parameters
$input : mixed

Rohwert aus dem Schreibvorgang.

Return values
array<string, mixed>

creationell_captcha_truncate_setting_text()

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.

Parameters
$value : string

Bereits vorsanitisierter Wert.

$max_chars : int

Obergrenze in Zeichen (nicht Bytes).

Return values
string

creationell_captcha_truncate_chars()

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:

  1. preg_match( '/^.{0,N}/us' ) — der schnelle Weg. s lässt „.“ auch Zeilenumbrüche treffen (relevant für textblock).
  2. 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.
  3. Ein Byte-Lauf über die UTF-8-Startbytes — braucht überhaupt keine Erweiterung und keine PCRE.

WARUM ES DIESE FUNKTION GIBT (Nachlese N6, Befund 7; Fehlerklasse 4)

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.

Parameters
$value : string

Gültiges UTF-8.

$max_chars : int

Obergrenze in Zeichen.

Tags
since
1.1.0
Return values
string

creationell_captcha_truncate_chars_by_bytes()

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).

Parameters
$value : string

Gültiges UTF-8.

$max_chars : int

Obergrenze in Zeichen.

Tags
since
1.1.0
Return values
string

creationell_captcha_sanitize_settings()

Sanitises the settings array before it is stored.

creationell_captcha_sanitize_settings(mixed $input[, string $context = CREATIONELL_CAPTCHA_SANITIZE_PROGRAMMATIC ]) : array<string, mixed>
Parameters
$input : mixed

Raw input from the settings form.

$context : string = CREATIONELL_CAPTCHA_SANITIZE_PROGRAMMATIC

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).

Return values
array<string, mixed>

creationell_captcha_sync_event_log_table()

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:

  • wp-admin (Formular, Import, Werksreset, „Standardwerte laden"): unverändert — die Tabelle entsteht weiterhin beim Speichern.
  • WP-CLI und jeder andere Kontext ohne 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).
  • Ein Speichern, das den Optionswert unverändert lässt: WordPress schreibt dann nichts und feuert keinen der beiden Hooks. Diese Lücke deckt seit der Nachlese N6 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.

creationell_captcha_store_settings()

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.

Parameters
$clean : array<string, mixed>

Bereits sanitisierter Wertesatz.

Tags
since
1.1.0

creationell_captcha_render_engine_section()

Renders the description shown at the top of the Proof-of-Work-Engine section.

creationell_captcha_render_engine_section() : void

creationell_captcha_render_widget_appearance_section()

Renders the description shown at the top of the widget-appearance section.

creationell_captcha_render_widget_appearance_section() : void

creationell_captcha_render_code_challenge_section()

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.

creationell_captcha_render_core_forms_section()

Renders the description shown at the top of the core-forms section.

creationell_captcha_render_core_forms_section() : void

creationell_captcha_render_interceptor_section()

Renders the description shown at the top of the interceptor section.

creationell_captcha_render_interceptor_section() : void

creationell_captcha_render_form_plugins_section()

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.

creationell_captcha_render_proxy_section()

Renders the description shown at the top of the proxy section.

creationell_captcha_render_proxy_section() : void

creationell_captcha_render_bypass_section()

Renders the description shown at the top of the bypass section.

creationell_captcha_render_bypass_section() : void

creationell_captcha_render_firewall_section()

Renders the description shown at the top of the firewall section.

creationell_captcha_render_firewall_section() : void

creationell_captcha_render_ratelimit_section()

Renders the description shown at the top of the rate-limiting section.

creationell_captcha_render_ratelimit_section() : void

creationell_captcha_render_underattack_section()

Renders the description shown at the top of the under-attack section.

creationell_captcha_render_underattack_section() : void

creationell_captcha_render_underattack_appearance_section()

Renders the description shown at the top of the under-attack appearance section.

creationell_captcha_render_underattack_appearance_section() : void

creationell_captcha_render_analytics_section()

Renders the description shown at the top of the analytics section.

creationell_captcha_render_analytics_section() : void

creationell_captcha_render_email_section()

Renders the description shown at the top of the email-protection section.

creationell_captcha_render_email_section() : void

creationell_captcha_render_field()

Renders a single settings field.

creationell_captcha_render_field(array<string, mixed> $args) : void
Parameters
$args : array<string, mixed>

Field arguments (key + field spec).

creationell_captcha_field_label()

Liefert das Label eines Einstellungsfeldes (Fallback: der Schlüssel selbst).

creationell_captcha_field_label(string $key) : string
Parameters
$key : string

Feldschlüssel.

Return values
string

creationell_captcha_field_spec()

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.

Return values
array<string, array<string, mixed>>

creationell_captcha_field_option_label()

Liefert das Options-Label eines `select`-Feldes (Fallback: der Rohwert).

creationell_captcha_field_option_label(string $key, string $value) : string
Parameters
$key : string

Feldschlüssel.

$value : string

Optionswert.

Return values
string

creationell_captcha_tools_redirect()

Stores a one-shot admin notice and redirects back to the Werkzeuge page.

creationell_captcha_tools_redirect(string $type, string $message) : never
Parameters
$type : string

'success' or 'error'.

$message : string

Notice text.

Return values
never

creationell_captcha_tools_guard()

Guards a tools action: requires manage_options and a valid nonce.

creationell_captcha_tools_guard(string $action) : void
Parameters
$action : string

The nonce action name.

creationell_captcha_handle_export_settings()

Streams the current settings as a JSON download.

creationell_captcha_handle_export_settings() : void

creationell_captcha_handle_import_settings()

Handles the settings-import upload.

creationell_captcha_handle_import_settings() : void

creationell_captcha_collect_settings_error_messages()

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.

Return values
array<int, string>

creationell_captcha_handle_reset_settings()

Handles the full factory reset.

creationell_captcha_handle_reset_settings() : void

creationell_captcha_handle_load_defaults()

Handles "load defaults" (keeps the lists).

creationell_captcha_handle_load_defaults() : void

creationell_captcha_handle_cloudflare_refresh()

Triggers a manual Cloudflare-range refresh from the Werkzeuge page.

creationell_captcha_handle_cloudflare_refresh() : void

creationell_captcha_handle_cloudflare_clear()

Empties the cached Cloudflare-range option from the Werkzeuge page.

creationell_captcha_handle_cloudflare_clear() : void

creationell_captcha_register_tools_page()

Registers the "Werkzeuge" submenu page under the CreaCaptcha menu.

creationell_captcha_register_tools_page() : void

creationell_captcha_render_tools_notice()

Renders the one-shot admin notice left behind by a tools action.

creationell_captcha_render_tools_notice() : void

creationell_captcha_render_tools_page()

Renders the "Werkzeuge" page.

creationell_captcha_render_tools_page() : void

creationell_captcha_render_cloudflare_status()

Renders the Cloudflare-cache status block inside the Werkzeuge tool card.

creationell_captcha_render_cloudflare_status() : void

creationell_captcha_run_under_attack()

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

creationell_captcha_maybe_upgrade()

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).

creationell_captcha_migrate_widget_mode()

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.

creationell_captcha_store_migrated_settings()

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).

Parameters
$settings : array<string, mixed>

Migrierter Wertesatz.

creationell_captcha_register_assets()

Registers the widget script and — for Argon2id — its worker registration.

creationell_captcha_register_assets() : void

creationell_captcha_safe_inline_css()

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.

Parameters
$css : string

Rohes CSS aus den Einstellungen.

Return values
string —

CSS, das den umgebenden <style>-Block nicht verlassen kann.

creationell_captcha_build_widget_markup()

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.

Return values
string

creationell_captcha_render_widget()

Renders the ALTCHA widget markup and enqueues its assets.

creationell_captcha_render_widget() : void

creationell_captcha_verify_payload()

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.

Parameters
$raw : string

The raw altcha payload.

Return values
bool

creationell_captcha_verify_request()

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.

Return values
bool

creationell_captcha_widget()

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 element so the hidden altcha field is submitted with the form.

creationell_captcha_get_widget_markup()

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).

Return values
string —

The widget markup.

creationell_captcha_widget_shortcode()

Shortcode handler for [creationell_captcha].

creationell_captcha_widget_shortcode() : string

Place the shortcode inside a element so the hidden altcha field is submitted with the form.

Return values
string —

The widget markup.

creationell_captcha_uninstall_site()

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.

creationell_captcha_uninstall_user_meta()

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.


        
On this page

Search results