Developer Reference
Hooks, public functions, database schema, transient keys, and internals for Login Delay Shield v2.6.0.
Architecture
Login Delay Shield is a single-plugin WordPress security plugin. Its main files:
| File | Purpose |
|---|---|
wp-login-delay.php | Main plugin file — all security logic, hooks, database management, dashboard widget |
wldelay-settings.php | Settings registration via WordPress Settings API, sanitization, validation |
wldelay-settings-view.php | LDS_Settings_View class — admin UI rendering (collapsible sections, status badges, summary box, telemetry, audit log) |
wldelay-features.php | LDS_Features — single source of truth for every option key, type, and default |
wldelay-pipeline.php | wldelay_process_failed_attempt() — the single failure-processing pipeline shared by wp-login, XML-RPC, REST, application-password, and password-reset |
wldelay-challenge.php | Challenge Mode — the LDS_Challenge_Provider base class, the three built-in providers, the filterable registry, per-IP challenge state, and the pre-password authentication gate |
wldelay-botnet.php | Distributed-attack (botnet) detection — per-username distinct-IP tracking, alert fan-out, recent-detection feed |
wldelay-recovery.php | Emergency Recovery URL — opt-in secret token that lets a locked-out admin clear their own IP lockout without admin or server access |
wldelay-persistence.php | Durable lockout store (WLDelay_DB_Persistence) and the lockout database table |
wldelay-audit.php | Admin/security audit log — table, write path, health/recovery tracking |
wldelay-async.php | Event firehose and deferred-task queue (shutdown/cron flush) |
wldelay-fail2ban.php | fail2ban log writing, path validation, and rotation |
wldelay-enumeration.php | Username/author enumeration hardening |
wldelay-privacy.php | GDPR personal-data exporter and eraser registration |
wldelay-migration.php | Settings-version migration runner |
wldelay-changelog.php | In-admin changelog page parsed from readme.txt |
admin.js | Admin JavaScript — collapsible sections, tooltips, and UI interactions |
admin.css | Admin styles for the settings page and dashboard widget |
uninstall.php | Uninstall routine — removes options, the log table, registered transients, and the plugin-owned fail2ban log directory on plugin deletion |
Failed-attempt counters live in WordPress transients. Active lockouts are written to a dedicated database table through a filterable persistence store (WLDelay_DB_Persistence), with a transient layer for fast lookups and a graceful fallback to transient-only state when the durable write fails. Three custom tables back the failed login log, the lockout store, and the admin/security audit log. A lightweight deferred-task queue moves non-critical writes (audit rows, expired-lockout purges) off the request hot path to shutdown or a daily cron backstop. Every authentication surface (wp-login, XML-RPC, REST, application passwords, password reset) routes failures through one pipeline, wldelay_process_failed_attempt(), which emits a failed_attempt event; distributed-attack detection subscribes to that event and defers its cross-IP query to the same queue. Custom Login URL uses WordPress rewrite rules. No custom REST API endpoints or post types are registered.
When the plugin is deleted, uninstall.php removes its options, the failed-login log table, registered transients, and the plugin-owned fail2ban log directory. The cleanup is multisite-aware and leaves any custom fail2ban log path untouched. Since v2.3.4.
Constants
| Constant | Value | Description |
|---|---|---|
WLDELAY_VERSION | '2.6.0' | Plugin version |
WLDELAY_PLUGIN_FILE | __FILE__ | Main plugin file path |
WLDELAY_OPTION_NAME | 'wldelay_options' | WordPress option name for all settings |
WLDELAY_DB_VERSION | '7' | Schema version, tracked in the wldelay_db_version option to drive dbDelta() upgrades |
Emergency Constants
Define these in wp-config.php to recover access. They are read with defined() — the plugin never sets them.
| Constant | Effect |
|---|---|
WLDELAY_SAFE_MODE | When true, disables every delay, lockout, and tracking path (login, XML-RPC, REST, application passwords, password reset) — the plugin behaves as if every IP were whitelisted. A persistent, non-dismissible admin notice shows while it is active. Since v2.4.0. |
WLDELAY_DISABLE_CUSTOM_LOGIN | When defined, bypasses the Custom Login URL feature so /wp-login.php works again. Since v2.4.0. |
Emergency Recovery Constants
Tuning constants for the Emergency Recovery URL. Each is defined with a defined() guard, so you can override it in wp-config.php. Since v2.6.0.
| Constant | Value | Description |
|---|---|---|
WLDELAY_RECOVERY_QUERY_VAR | 'wldelay_recovery' | Query var carrying the recovery token: /?wldelay_recovery=<token> |
WLDELAY_RECOVERY_NAG_DAYS | 90 | Token age (days) after which the settings page nags you to rotate it |
WLDELAY_RECOVERY_RL_MAX | 5 | Failed token attempts allowed per IP inside the rate-limit window |
WLDELAY_RECOVERY_RL_WINDOW | 900 | Rate-limit window in seconds (15 minutes) |
WLDELAY_RECOVERY_REVEAL_TTL | 300 | Seconds the freshly generated URL stays available to view/download (5 minutes) |
Filter Hooks
wp_authenticate_user
filter · priority 1Main authentication filter. Applies delays and lockouts to login attempts. Parameters: $user (WP_User|WP_Error), $password. Returns WP_Error if locked out, otherwise applies sleep() delay on failure and returns $user.
rest_authentication_errors
filter · priority 20REST API authentication protection. Applies delays and lockout to failed REST API authentication when wldelay_rest_enabled is on. Returns a 403 WP_Error if the IP is locked out. Respects the IP whitelist and skips application-password attempts (handled separately). Since v2.1.2.
authenticate
filter · priority 25Application-password authentication protection. Applies delays and lockout to failed Basic Auth attempts when wldelay_application_password_enabled is on. Detects app-password attempts via PHP_AUTH_USER/PHP_AUTH_PW server headers. Parameters: $user, $username, $password. Since v2.1.2.
authenticate
filter · priority 99Blocks XML-RPC authentication when the “block XML-RPC” setting is enabled. Parameters: $user, $username, $password. Returns WP_Error('wldelay_xmlrpc_blocked') for XML-RPC requests.
wldelay_2fa_providers
filterCustomize the list of recognized 2FA plugins for the settings-page health check. Receives an associative array of slug => [ plugin_basename, … ]. Return the modified array to add or remove providers. The default map includes two-factor, wp-2fa, mini-orange, and google-authenticator. Since v2.1.4.
wldelay_2fa_coverage_checkers
filterRegister provider-specific callbacks that report privileged-user 2FA coverage for the settings-page notice. Receives a map of provider_slug => callable (the built-in entry covers the two-factor plugin). Each callback returns an array with keys supported, privileged_total, protected, unprotected, and unknown.
wldelay_normalize_username
filterCustomize how usernames are normalized for failure tracking and lockout identity. Receives the normalized username and the original input. Return your own normalized value to support LDAP, email-as-login, or SSO backends where the default lowercase/sanitize behavior is not enough. Since v2.3.0.
wldelay_persistence_store
filterSwap the lockout persistence backend. Receives null; return a WLDelay_Persistence implementation to replace the default DB-backed store (for example a Redis or external store) without touching call sites. Falls back to WLDelay_DB_Persistence when the filter returns anything else.
wldelay_login_help_url
filterSet the “Need help getting in?” link shown in the login-page lockout feedback block. Defaults to the site’s lost-password URL (respecting any custom-login-slug rewrite); point it at a support or docs page instead. Returned value is escaped at output.
wldelay_fail2ban_allowed_log_dirs
filterExtend the set of directories a custom fail2ban log path may live in. By default only the plugin-owned protected directory is allowed; add server-protected directories here. Web-server protection files (.htaccess, index.html, index.php) are written to every allowed directory. A custom path outside the allowed set disables logging rather than writing elsewhere.
wldelay_fail2ban_max_log_bytes
filterControl the size at which the fail2ban log is rotated to a single .log.1 backup. Defaults to 5 MB. Return 0 to disable in-plugin rotation and rely on system logrotate instead. Since v2.3.4.
wldelay_botnet_ip_threshold
filterNumber of distinct source IPs targeting one username that triggers a distributed-attack alert. The stored option is clamped to [2, 100] before the filter runs, and the result is re-floored at 2 so a filter cannot drop below the minimum. Default 5. Since v2.5.0.
wldelay_botnet_window
filterSliding detection window, in seconds. The stored minutes option is clamped to [5, 60] and converted to seconds before the filter; the result is floored at MINUTE_IN_SECONDS. Default 900 (15 minutes). Since v2.5.0.
wldelay_botnet_alert_cooldown
filterPer-username cooldown, in seconds, that suppresses repeat distributed-attack alerts after one fires. Default HOUR_IN_SECONDS. Since v2.5.0.
wldelay_cloudflare_ip_ranges
filterThe list of Cloudflare CIDR ranges used to validate the CF-Connecting-IP header. The header is honored only when REMOTE_ADDR falls inside one of these ranges, so it cannot be spoofed. Return your own array to extend or replace the built-in IPv4/IPv6 ranges. Since v2.4.0.
wldelay_resolve_country_code
filterSupplies the visitor country for the optional Country Blocking feature. Return an ISO 3166-1 alpha-2 code such as US or DE (case-insensitive; normalized internally). Parameters: $country_code, $ip (client IP after proxy handling), $source (auth source label: wp-login, xmlrpc, rest, application-password). The incoming value is always empty: your resolver runs first and wins. Only when it returns an empty string does the plugin fall back to its built-in detection (wldelay_detect_country_from_request(), since v2.7.0), which reads, in order, Cloudflare’s CF-IPCountry (trusted proxy headers + a verified Cloudflare peer), $_SERVER['GEOIP_COUNTRY_CODE'], then an X-Country-Code proxy header (trusted proxy headers). A resolver is therefore never handed a visitor-supplied header value to defer to. The plugin still ships no GeoIP database and performs no network lookup. The block is asserted at several points in the auth chain, so this filter can run more than once per attempt; cache any expensive lookup per request. Whitelisted IPs and safe mode always bypass the check. Since v2.6.0.
wldelay_trust_server_country_variable
filterWhether a bare $_SERVER['GEOIP_COUNTRY_CODE'] is trusted by the built-in country resolver. Default true: PHP exposes request headers as HTTP_*, so an unprefixed variable normally can only come from the web server’s own GeoIP module. Return false on a deployment that deliberately maps client input into a bare CGI parameter (an unusual fastcgi_param or SetEnvIf rule). Since v2.7.0.
wldelay_challenge_providers
filterThe challenge-provider registry for Challenge Mode, keyed by provider id. Add your own entry to offer a custom challenge in the settings dropdown; each value must be an instance of LDS_Challenge_Provider (see Challenge Providers). Entries that are not LDS_Challenge_Provider instances are dropped. Removing the built-in math provider is safe — the resolver falls back to a fresh instance of it rather than returning null. Since v2.7.0.
wldelay_send_custom_login_email
filterWhether to email the new login URL to the site admin when the Custom Login URL feature is enabled or its slug changes. Default true; return false (e.g. __return_false) to suppress the recovery email. Since v2.4.0.
wldelay_help_base_url
filterBase URL for the “Learn more” documentation links on each settings card. Defaults to https://damoiseau.xyz/docs/login-delay-shield/user-guide/. Return an empty string to hide the links, or point them at your own mirror. Since v2.5.0.
wldelay_export_login_log_should_exit
filterControls whether the CSV export handler calls exit() after streaming. Default true. Set to false in test environments. Since v2.1.2.
plugin_action_links_{basename}
filterAdds a “Settings” link to the plugin’s entry on the Plugins page.
Action Hooks
wp_login_failed
actionHooked to log failed login attempts to the database. Parameters: $username. Records the IP address, username, timestamp, and source (wp-login, xmlrpc, rest, application-password, or password-reset).
wldelay_cleanup_logs
action · customCustom cron hook that runs daily. Deletes log entries older than the configured retention period. Processes in batches of 1,000 rows.
wp_dashboard_setup
actionRegisters the “Recent Failed Login Attempts” dashboard widget.
admin_post_wldelay_unlock_current_ip
actionHandles the “Unlock Current IP” button on the settings page. Removes the lockout and failure counters for the requesting admin’s IP address. Nonce-protected and requires manage_options capability.
admin_post_wldelay_export_login_log
actionHandles CSV export of the failed login log. Streams results in 1,000-row batches with formula-injection protection. Reads optional filters from $_GET (source, IP, username, date range). Nonce-protected and requires manage_options capability. Since v2.1.2.
admin_post_wldelay_recovery_generate
actionMints a new Emergency Recovery token, stores its hash, emails the URL to the site admin, and sets the one-time reveal. Nonce-protected, requires manage_options. Since v2.6.0.
admin_post_wldelay_recovery_download
actionStreams the once-revealed recovery URL as a .txt download. Nonce-protected, requires manage_options; only works inside the reveal window. Since v2.6.0.
admin_post_wldelay_recovery_confirm / admin_post_nopriv_wldelay_recovery_confirm
actionHandles the confirm POST from the recovery landing page. Re-validates the token and nonce, then clears the caller’s own IP lockout (it never logs the user in). Registered for both authenticated and unauthenticated callers so a locked-out admin can use it. Since v2.6.0.
wldelay_event_{event}
action · customPer-event hook fired for each plugin event (e.g. wldelay_event_cron_tick). Receives the event payload array. Subscribe with wldelay_on_event(), or hook the generic wldelay_event action (which receives $event_name, $payload) to observe every event as an audit/SIEM firehose.
wldelay_persistence_write_failed
action · customFires when a lockout could not be written to the durable store. Parameters: $ip, $type (login or password-reset). The lockout falls back to transient-only state until the store recovers.
wldelay_audit_write_failed
action · customFires when an audit-log row could not be written. Parameters: $action (the action key whose row was lost), $error (DB error string).
Error Codes
| Error Code | Context | Description |
|---|---|---|
wldelay_ip_locked | Login form | IP or IP+username is locked out. Message includes a countdown. |
wldelay_xmlrpc_blocked | XML-RPC | XML-RPC authentication is disabled on this site. |
wldelay_attempts_remaining | Login form | Shows remaining attempts before lockout (appended to existing error). |
wldelay_country_blocked | All auth paths | The country detected for the request is on the blocked list. Not counted as a failed attempt. |
wldelay_challenge_required | Login form, REST / app password | Challenge mode is in force for this IP. On the login form a challenge is issued and rendered; on non-interactive paths it is a hard block (HTTP 403 for application passwords). Not counted as a failed attempt. (v2.7.0+) |
wldelay_challenge_unavailable | Login form | A challenge is required but the active provider cannot run for this account (e.g. the email provider found no address). Not counted as a failed attempt. (v2.7.0+) |
wldelay_challenge_failed | Login form | The submitted answer was wrong. Counted as a failed attempt, unlike the two above. (v2.7.0+) |
Core Functions
| Function | Returns | Description |
|---|---|---|
wldelay_get_options() | array | Get cached plugin options. Uses a global cache to avoid repeated get_option() calls. |
wldelay_clear_options_cache() | void | Clear the in-memory options cache. Called automatically when settings are updated. |
wldelay_get_delay_value( $failure_count ) | int | Calculate the delay in seconds based on current settings (fixed, random, or progressive) and the number of failures. |
wldelay_get_client_ip() | string | Get the client IP address. Proxy headers are read only when wldelay_trust_proxy_headers is enabled: CF-Connecting-IP (honored only when REMOTE_ADDR is a Cloudflare edge), X-Sucuri-ClientIP, Client-IP, X-Real-IP, then X-Forwarded-For. Each candidate is validated as an IP; a garbage value falls through to REMOTE_ADDR. |
wldelay_get_cloudflare_ip_ranges() | array | Built-in Cloudflare IPv4/IPv6 CIDR ranges, filterable via wldelay_cloudflare_ip_ranges. Since v2.4.0. |
wldelay_is_cloudflare_remote_addr( $remote_addr ) | bool | Whether the TCP peer is within a Cloudflare range (gates CF-Connecting-IP trust). Since v2.4.0. |
wldelay_detect_proxy_headers() | array | Human-readable names of any proxy/CDN forwarding headers present on the current request. Since v2.4.0. |
wldelay_get_proxy_health_status() | array | Returns status (misconfigured-cdn, spoofable, ok, or none) and the detected headers, powering the settings-page proxy health notice. Since v2.4.0. |
wldelay_process_failed_attempt( $username, $source, $context ) | void | Unified failure-processing pipeline shared by all authentication surfaces. Logs the attempt, updates counters/lockout, and emits the failed_attempt event. Since v2.5.0. |
wldelay_get_lockout_attempt_strategy( $options ) | string | Returns 'ip' or 'ip_username'. |
wldelay_normalize_username( $username ) | string | Lowercases and sanitizes a username for consistent tracking. |
wldelay_get_attempt_identifier( $ip, $username, $options ) | string | Returns $ip or $ip|$username based on strategy. |
wldelay_get_requested_login_username() | string | Gets the normalized username from $_POST['log']. |
wldelay_is_xmlrpc_request() | bool | Checks XMLRPC_REQUEST constant and REQUEST_URI. |
wldelay_is_rest_request() | bool | Checks if the current request is a WordPress REST API request. Since v2.1.2. |
wldelay_is_application_password_attempt() | bool | Checks for PHP_AUTH_USER and PHP_AUTH_PW server headers (Basic Auth). Since v2.1.2. |
wldelay_get_login_source() | string | Returns 'xmlrpc', 'rest', 'application-password', 'password-reset', or 'wp-login'. |
wldelay_get_login_source_label( $source ) | string | Human-readable label for a source key, used by the dashboard widget and log views. |
wldelay_custom_login_is_active() | bool | Whether the Custom Login URL feature is enabled and the slug is valid. Since v2.2.3. |
wldelay_get_custom_login_slug() | string | Returns the configured custom login slug (default 'my-login'). Since v2.2.3. |
Lockout Functions
| Function | Returns | Description |
|---|---|---|
wldelay_is_ip_locked( $ip, $username ) | bool | Check if an IP (or IP+username) is currently locked out. |
wldelay_lock_ip( $ip, $username ) | void | Lock an IP/username. Sets a transient with the lockout duration. |
wldelay_get_failure_count( $ip, $username ) | int | Get the current failure count for an IP/username. |
wldelay_get_lockout_remaining_seconds( $ip, $username ) | int | Seconds remaining on a lockout (0 if not locked). |
wldelay_get_lockout_duration_seconds( $options ) | int | Get the configured lockout duration in seconds. |
wldelay_get_lockout_error_message( $ip, $username ) | string | Build the lockout error message with human-readable countdown. |
wldelay_track_failed_attempt( $username ) | int | Increment failure counter, trigger lockout and/or email if thresholds are met. |
wldelay_send_notification_email( $ip, $username, $attempts ) | void | Send email alert (respects site-wide cooldown). |
Whitelist Functions
| Function | Returns | Description |
|---|---|---|
wldelay_is_ip_whitelisted( $ip ) | bool | Check if an IP is in the whitelist. Returns false if whitelist is disabled. |
wldelay_ip_in_range( $ip, $range ) | bool | Check if an IP matches a single address or CIDR range. Supports both IPv4 and IPv6. |
Recovery Functions
| Function | Returns | Description |
|---|---|---|
wldelay_delete_lockout_for_ip( $ip ) | int | Remove all lockout and failure transients for a specific IP. Returns the count of removed entries. Cleans up both ip and ip_username strategy keys. |
wldelay_flush_lockout_transients() | int | Clear all lockout and failure transients site-wide. Uses the transient registry and falls back to a database scan. Returns total entries removed. |
wldelay_register_transient_key( $key ) | void | Add a transient key to the registry (wldelay_transient_registry option). Called automatically when failure/lockout transients are created. |
wldelay_unregister_transient_key( $key ) | void | Remove a transient key from the registry. Called automatically during cleanup. |
Botnet Detection Functions
Distributed-attack detection (wldelay-botnet.php) watches distinct source IPs per targeted username inside a sliding window and alerts — it never blocks. The COUNT query is deferred to the shutdown task queue off the request hot path. Since v2.5.0.
| Function | Returns | Description |
|---|---|---|
wldelay_botnet_is_enabled() | bool | Whether distributed-attack detection is on (default on). |
wldelay_botnet_get_ip_threshold() | int | Distinct-IP threshold that triggers an alert. Clamped to [2, 100]; default 5. |
wldelay_botnet_get_window_seconds() | int | Detection window in seconds. Option clamped to [5, 60] minutes; default 900. |
wldelay_botnet_get_recent_detections() | array | Rolling list of recent detections (newest first, up to 20, 24h TTL) that feeds the dashboard banner. |
On detection the plugin writes a synchronous botnet_detected audit-log row, pushes an entry onto the dashboard feed, and — when email notifications are enabled — sends an alert email naming the targeted username and up to five sample IPs.
Challenge Providers
Challenge Mode (wldelay-challenge.php) gates authentication on a self-hosted challenge once an IP is over the configured failure threshold. The gate hooks wp_authenticate_user — after the username resolves to a WP_User but before wp_check_password() — so a rejection cannot be clobbered by core’s later credential re-check and password validity never leaks. Non-interactive sources (XML-RPC, REST, application passwords) are denied outright. Two extra guards cover paths that skip wp_authenticate_user: native REST application-password auth is checked on rest_authentication_errors (cookie-authenticated users are left alone), and XML-RPC application passwords — which core’s authenticator returns without firing wp_authenticate_user — hit a late authenticate backstop at priority 9999. When the login is an email address, core fires wp_authenticate_user on both the username and email paths; a solved challenge is remembered for the rest of that same chain so the second pass doesn’t re-challenge. Since v2.7.0.
A provider extends LDS_Challenge_Provider and implements four methods, plus an optional availability check:
| Method | Returns | Description |
|---|---|---|
id() | string | Stable provider id — the settings dropdown value and the key it is registered under. |
label() | string | Translated dropdown label. |
is_available( $username, $ip ) | bool | Whether the provider can run for this subject right now. Default true; the email provider returns false when the username has no resolvable address, which turns into a wldelay_challenge_unavailable rejection rather than a pass. |
issue( $username, $ip ) | array | Create the challenge and return the state to persist (hash the answer — state is stored verbatim in a transient). |
render( array $state ) | void | Echo the challenge fields on the login form. Input name is wldelay_challenge_response; keep it WCAG AA. |
verify( $input, array $state, $username, $ip ) | bool | Verify the submitted response against the persisted state. Compare with hash_equals(). |
Built-in providers: math (LDS_Math_Challenge_Provider, single-digit addition, the default), email (LDS_Email_Challenge_Provider, 6-digit wp_mail() code, 5 sends per 10 minutes both per IP and per recipient), and pow (LDS_Pow_Challenge_Provider, browser SHA-256 proof-of-work, DIFFICULTY = 4 leading hex zeros; needs a secure context, see LDS_Pow_Challenge_Provider::is_secure_context() — HTTPS or a localhost host). Register your own through the wldelay_challenge_providers filter.
| Function | Returns | Description |
|---|---|---|
wldelay_get_challenge_providers() | array | The provider registry keyed by id, after the filter above. |
wldelay_get_challenge_provider( $id ) | object|null | One LDS_Challenge_Provider by id, or null when unregistered. |
wldelay_get_active_challenge_provider( $options = null ) | object | The LDS_Challenge_Provider selected in settings. Falls back to math for an unknown id, and when pow is selected but the login page is not a secure context (no crypto.subtle on plain HTTP). Falls back to a fresh LDS_Math_Challenge_Provider if a filter removed even that — never null. |
wldelay_get_challenge_mode_threshold( $options = null ) | int | Sanitized failed-attempt threshold, clamped to [1, 100]. Default 3. |
wldelay_challenge_required_for_count( $failure_count, $options = null ) | bool | Pure predicate: challenge mode enabled and the count at or over the threshold. |
wldelay_is_challenge_required( $username = '', $ip = null, $options = null ) | bool | Whether the current subject must be challenged. Uses the same failure counter and ip / ip_username strategy as lockout, and honors the whitelist and safe-mode bypasses. |
wldelay_issue_challenge( $provider, $username, $ip ) | void | Mint a challenge and persist its state, tagged with the provider id and the account it was issued for. |
wldelay_get_challenge_state( $ip ) / wldelay_set_challenge_state( $ip, $state ) / wldelay_clear_challenge_state( $ip ) | array / void / void | Read, write, and drop the pending per-IP challenge state (10-minute transient). A challenge is consumed before verification, so a solved one cannot be replayed; a wrong answer restores it for up to 5 tries. |
wldelay_challenge_state_key( $ip ) | string | Transient key for an IP’s challenge state. |
wldelay_render_challenge_field() | void | Render the pending challenge’s fields on the login form. |
fail2ban Config & Coherence
| Function | Returns | Description |
|---|---|---|
wldelay_fail2ban_generate_filter_config() | string | Builds the fail2ban filter.d config (with the current failregex) for download. Since v2.5.0. |
wldelay_fail2ban_generate_jail_config() | string | Builds the matching jail.local stanza for the active log path. Since v2.5.0. |
wldelay_handle_fail2ban_config_download() | void | admin-post handler that streams the generated filter/jail config as a download. Nonce-protected, requires manage_options. Since v2.5.0. |
wldelay_buffer_fail2ban_line( $event, $ip, $username, $source ) | void | Buffers a fail2ban line for the request instead of writing immediately. Since v2.5.0. |
wldelay_flush_fail2ban_buffer() | void | Writes the buffered fail2ban lines once per request (one disk write instead of one per attempt). Since v2.5.0. |
wldelay_settings_coherence_warnings( $options ) | array | Returns non-blocking warnings for contradictory settings (empty whitelist, unwritable fail2ban path, alert thresholds that can never fire), shown on the settings page. Since v2.5.0. |
Emergency Recovery URL
An opt-in, time-boxed secret URL (/?wldelay_recovery=<token>) that lets a locked-out admin clear their own IP lockout with no admin or server access. Only the token’s sha256 hash is stored; the raw token is shown once. A GET renders a confirm page and the unlock fires from a nonce-protected POST, so email and antivirus link-prefetchers cannot trigger it. It never logs the user in and never disables the shield. Since v2.6.0.
| Function | Returns | Description |
|---|---|---|
wldelay_recovery_is_enabled() | bool | Whether the Emergency Recovery URL feature is switched on (default off). |
wldelay_recovery_generate_token() | string | Mint a 256-bit token, store only its sha256 hash plus the generation timestamp, and return the raw token (the only time it exists in plaintext). |
wldelay_recovery_hash( $token ) | string | sha256 hex of a raw token. |
wldelay_recovery_token_matches( $token ) | bool | Constant-time (hash_equals) comparison of a candidate token against the stored hash. |
wldelay_recovery_build_url( $token ) | string | Build the full recovery URL for a raw token. |
wldelay_recovery_generated_age_days() | int|null | Age of the current token in whole days, or null when never generated. |
wldelay_recovery_needs_rotation() | bool | Whether the token is older than WLDELAY_RECOVERY_NAG_DAYS (90) and should be rotated. |
wldelay_recovery_rate_limit_hit( $ip ) | bool | Record a failed attempt for an IP and report whether it is now over the limit (WLDELAY_RECOVERY_RL_MAX per WLDELAY_RECOVERY_RL_WINDOW). Only failed attempts count. |
wldelay_recovery_send_email( $url ) | void | Email the recovery URL to the site admin address. |
The feature audits every event through the audit log: recovery_url_generated, recovery_used, recovery_failed, and recovery_rate_limited. Handlers run on the admin_post_* hooks listed under Action Hooks.
Database Functions
| Function | Returns | Description |
|---|---|---|
wldelay_get_log_table_name() | string | Returns the full table name: {$wpdb->prefix}wldelay_login_log. |
wldelay_create_log_table() | void | Creates or upgrades the log table using dbDelta(). Called on activation and version upgrades. |
wldelay_log_failed_attempt( $ip, $username, $source ) | void | Inserts a row into the log table and invalidates the dashboard cache. |
wldelay_get_recent_failed_attempts( $limit ) | array | Returns recent failed attempts ordered by time (used by the dashboard widget). |
wldelay_cleanup_old_logs() | void | Deletes entries older than the retention period in batches of 1,000. |
Export & Log Query Functions
Added in v2.1.2–2.2.3 for CSV export, filtered log queries, and trend analytics.
| Function | Returns | Description |
|---|---|---|
wldelay_get_login_log_attempts( $limit, $offset, $filters ) | array | Query log entries with optional filters (source, IP, username, date range) and pagination. All filters are applied at the SQL level. |
wldelay_get_export_login_log_url( $filters ) | string | Build a nonce-protected URL for the CSV export endpoint. Accepts optional filter array. |
wldelay_handle_export_login_log() | void | Main CSV export handler. Streams results in 1,000-row batches with flush() after each batch. Sets Content-Type: text/csv and Content-Disposition: attachment headers. |
wldelay_sanitize_login_log_filters( $filters ) | array | Validate and sanitize filter input. Checks IP format, date format (Y-m-d), and auto-swaps reversed date ranges. |
wldelay_get_login_log_filters_from_request() | array | Extract filters from $_GET using expected keys only (wldelay_log_source, wldelay_log_ip, wldelay_log_username, wldelay_log_from, wldelay_log_to). |
wldelay_csv_sanitize_cell( $value ) | string | Mitigate CSV formula injection by prepending a single quote to values starting with =, +, -, @, or whitespace. |
wldelay_get_top_ips( $days, $limit ) | array | Returns the top attacking IP addresses over the last $days days (default 7), limited to $limit results (default 10). Each row has ip_address and attempt_count. Since v2.2.3. |
wldelay_get_top_usernames( $days, $limit ) | array | Returns the most targeted usernames over the last $days days (default 7), limited to $limit results (default 10). Each row has username and attempt_count. Since v2.2.3. |
wldelay_get_daily_attempts( $days ) | array | Returns daily failed attempt totals for the last $days days (default 7). Each row has day (Y-m-d) and attempt_count. Since v2.2.3. |
Database Schema
{$wpdb->prefix}wldelay_login_log
| Column | Type | Key | Description |
|---|---|---|---|
id | bigint(20) unsigned | PRIMARY | Auto-increment ID |
ip_address | varchar(45) | INDEX | IP address (supports IPv4 and IPv6) |
username | varchar(60) | INDEX | Username attempted |
attempted_at | datetime | INDEX | Timestamp of the attempt |
source | varchar(20) | INDEX | wp-login, xmlrpc, rest, application-password, or password-reset |
username_attempted | — | COMPOSITE INDEX | Composite (username, attempted_at) index — added in schema gen 7 to count distinct IPs per username within a time window for distributed-attack detection |
Created on plugin activation via dbDelta(). Schema version is tracked in the wldelay_db_version option.
{$wpdb->prefix}wldelay_lockouts
Durable lockout store behind WLDelay_DB_Persistence. Lockouts survive object-cache eviction and transient expiry; the transient_key column links a row to its companion transient.
| Column | Type | Key | Description |
|---|---|---|---|
id | bigint(20) unsigned | PRIMARY | Auto-increment ID |
lockout_key | varchar(64) | UNIQUE | Hashed attempt identifier |
ip_address | varchar(45) | INDEX | IP address (IPv4 or IPv6) |
username | varchar(255) | — | Username, for the ip_username strategy |
lockout_type | varchar(20) | — | login or password-reset |
source | varchar(20) | — | Originating auth surface |
transient_key | varchar(191) | — | Companion transient name |
generation | varchar(32) | — | Generation token for bulk-clear matching |
created_at | datetime | — | When the lockout was set |
expires_at | datetime | INDEX | When the lockout expires |
{$wpdb->prefix}wldelay_audit_log
Admin/security audit trail. Rows record settings changes, lockout clears, and whitelist edits. Writes are deferred to shutdown through the task queue so they stay off the request hot path.
| Column | Type | Key | Description |
|---|---|---|---|
id | bigint(20) unsigned | PRIMARY | Auto-increment ID |
actor_id | bigint(20) unsigned | INDEX | WP user ID, or 0 for system |
actor_login | varchar(60) | — | Actor login name |
action | varchar(50) | INDEX | Action key, e.g. settings_changed |
object | varchar(191) | — | The thing acted on |
old_value | text | — | Previous value (JSON for arrays) |
new_value | text | — | New value (JSON for arrays) |
ip_address | varchar(45) | — | Actor IP address |
created_at | datetime | INDEX | When the action occurred |
Transient Keys
All transient keys use MD5 hashing of the attempt identifier for security (prevents enumeration).
| Key Pattern | TTL | Value | Description |
|---|---|---|---|
wldelay_fails_{md5(id)} | 1 hour | int (count) | Failed attempt counter per IP or IP+username |
wldelay_lockout_{md5(id)} | Lockout duration | int (timestamp) | Lockout start time per IP or IP+username |
wldelay_email_cooldown | Cooldown minutes | int (timestamp) | Site-wide email rate limiter |
wldelay_dash_recent | 1 minute | array | Cached recent failed attempts for the dashboard widget |
wldelay_dash_trends | 5 minutes | array | Cached 7-day trend snapshot for the dashboard widget |
wldelay_botnet_detections | 24 hours | array | Rolling feed of recent distributed-attack detections (newest first, max 20) for the dashboard banner (v2.5.0+) |
wldelay_botnet_cd_{md5(username)} | 1 hour | int (timestamp) | Per-username alert cooldown that suppresses repeat distributed-attack alerts (v2.5.0+) |
wldelay_challenge_{md5(ip)} | 10 minutes | array | Pending challenge state for an IP: provider id, the account it was issued for, the answer hash, and the wrong-answer counter (abandoned after 5) (v2.7.0+) |
wldelay_challenge_email_rl_{md5(ip)} | 10 minutes | int (count) | Emailed one-time codes sent to an IP, capped at 5 per window (v2.7.0+) |
wldelay_challenge_email_to_{md5(email)} | 10 minutes | int (count) | Emailed one-time codes sent to one recipient, capped at 5 per window, so many IPs can’t take turns flooding one inbox (v2.7.0+) |
wldelay_recovery_rl_{md5(ip)} | 15 minutes | int (count) | Failed recovery-token attempts per IP, for the rate limiter (v2.6.0+) |
wldelay_recovery_reveal_{user_id} | 5 minutes | string (URL) | One-time reveal of a freshly generated recovery URL for the settings page / .txt download (v2.6.0+) |
The id in failure and lockout keys is the attempt identifier: either the IP address (ip strategy) or IP|username (ip_username strategy).
Options Reference
All settings are stored in a single serialized array under the wldelay_options WordPress option.
| Key | Type | Default | Description |
|---|---|---|---|
wldelay_delay | int | 1 | Fixed delay in seconds (0–10) |
wldelay_delay_random | bool | false | Use random delay |
wldelay_delay_random_min | int | 1 | Min random delay (1–10) |
wldelay_delay_random_max | int | 5 | Max random delay (1–10) |
wldelay_progressive_enabled | bool | false | Enable progressive delay |
wldelay_progressive_increment | int | 1 | Increment per attempt (1–10) |
wldelay_progressive_max | int | 30 | Max progressive delay (5–60) |
wldelay_email_enabled | bool | false | Enable email notifications |
wldelay_email_threshold | int | 5 | Failures before email (1–100) |
wldelay_email_address | string | '' | Notification email (fallback: admin_email) |
wldelay_email_cooldown | int | 5 | Minutes between emails (0–60) |
wldelay_lockout_enabled | bool | false | Enable IP lockout |
wldelay_lockout_threshold | int | 10 | Failures before lockout (1–100) |
wldelay_lockout_duration | int | 60 | Lockout duration in minutes (1–1440) |
wldelay_lockout_attempt_strategy | string | 'ip' | 'ip' or 'ip_username' |
wldelay_trust_proxy_headers | bool | false | Trust X-Forwarded-For headers |
wldelay_whitelist_enabled | bool | false | Enable IP whitelist |
wldelay_whitelist_ips | string | '' | Newline-separated IPs/CIDR ranges |
wldelay_log_retention_days | int | 30 | Log retention in days (0–365, 0 = forever) |
wldelay_xmlrpc_enabled | bool | false | Enable XML-RPC protection |
wldelay_xmlrpc_block | bool | false | Block all XML-RPC auth |
wldelay_rest_enabled | bool | false | Enable REST API protection (opt-in, injected default) (v2.1.2+) |
wldelay_application_password_enabled | bool | false | Enable application-password protection (opt-in, injected default) (v2.1.2+) |
wldelay_password_reset_enabled | bool | false | Enable password-reset protection (opt-in) (v2.3.2+) |
wldelay_enumeration_hardening_enabled | bool | false | Enable username/author enumeration hardening (opt-in) |
wldelay_fail2ban_enabled | bool | false | Enable fail2ban log writing (opt-in) (v2.3.4+) |
wldelay_fail2ban_log_path | string | '' | Custom fail2ban log path; empty uses the plugin-owned protected directory (v2.3.4+) |
wldelay_fail2ban_include_lockouts | bool | true | Also write lockout lines, not just failed-login lines (v2.3.4+) |
wldelay_botnet_enabled | bool | true | Distributed-attack detection — alert-only, never blocks (injected default) (v2.5.0+) |
wldelay_botnet_ip_threshold | int | 5 | Distinct IPs per username before an alert (clamped 2–100) (v2.5.0+) |
wldelay_botnet_window_minutes | int | 15 | Detection window in minutes (clamped 5–60) (v2.5.0+) |
wldelay_protection_profile | string | '' | Active Security Setup Wizard profile (conservative, balanced, aggressive, or empty/custom) (v2.3.3+) |
wldelay_custom_login_enabled | bool | false | Enable custom login URL (v2.2.3+) |
wldelay_custom_login_slug | string | 'my-login' | Custom login URL slug (lowercase, numbers, hyphens only) (v2.2.3+) |
wldelay_challenge_mode_enabled | bool | false | Enable Challenge Mode (opt-in, injected default) (v2.7.0+) |
wldelay_challenge_mode_threshold | int | 3 | Failed attempts before a challenge is required (clamped 1–100) (v2.7.0+) |
wldelay_challenge_mode_provider | string | 'math' | Active challenge provider id (math, email, pow, or a registered custom id; unknown values fall back to math) (v2.7.0+) |
wldelay_recovery_enabled | bool | false | Enable the Emergency Recovery URL (opt-in) (v2.6.0+) |
wldelay_recovery_token_hash | string | '' | sha256 hash of the current recovery token (never the raw token) (v2.6.0+) |
wldelay_recovery_generated_at | string | '' | UTC timestamp the token was generated (drives the rotation nag) (v2.6.0+) |
wldelay_recovery_last_used_at | string | '' | UTC timestamp the recovery URL was last used (v2.6.0+) |
Other WordPress Options
| Option Name | Description |
|---|---|
wldelay_db_version | Database schema version (tracks WLDELAY_VERSION) |
wldelay_plugin_version | Current plugin version |
wldelay_previous_version | Previous version (for upgrade detection) |
wldelay_name_change_notice_dismissed | Whether the rename notice was dismissed |
wldelay_whats_new_dismissed | Version whose “What’s New” notice was dismissed |
wldelay_settings_version | Settings-schema version, drives the migration runner |
wldelay_fail2ban_default_token | Random token for the plugin-owned fail2ban log directory name |
wldelay_transient_registry | Array of active transient keys (used by flush-lockouts for cleanup) |
Cron Jobs
| Hook | Schedule | Function | Description |
|---|---|---|---|
wldelay_cleanup_logs | Daily | wldelay_cleanup_old_logs() | Deletes log entries older than the retention period. Processes in batches of 1,000 to avoid table locks. Scheduled on wp hook, unscheduled on deactivation. |
wldelay_async_cron | Daily | wldelay_run_async_cron() | Emits the cron_tick event and flushes the deferred-task queue, giving expired-lockout purges and other queued work a backstop on low-traffic sites. Scheduled on wp hook, cleared on deactivation. |
WP-CLI
Login Delay Shield registers commands under the wp-login-delay namespace. Available when WP-CLI is installed.
| Command | Arguments | Description |
|---|---|---|
wp wp-login-delay unlock-ip <ip> | <ip> — IPv4 or IPv6 address | Removes lockout and failure transients for the specified IP. Validates IP format. Returns count of removed entries. |
wp wp-login-delay flush-lockouts | — | Clears all lockout and failure transients site-wide. Uses the transient registry and falls back to a database scan. Also clears the registry itself. |