emacom / bot-shield-core
Framework-agnostic core of BotShield: filter-count challenge with Cloudflare Turnstile and signed cookies
Requires
- php: ~8.2.0||~8.3.0||~8.4.0||~8.5.0
- psr/clock: ^1.0
- psr/log: ^1.1||^2.0||^3.0
- psr/simple-cache: ^1.0||^2.0||^3.0
Requires (Dev)
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^9.6
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Framework-agnostic BotShield core: challenge decision, Turnstile verification, signed cookie and bot verification via rDNS. The platform (Magento, WooCommerce, PrestaShop) provides the port implementations and the integration point.
Requirements
- PHP 8.2–8.5
psr/log,psr/simple-cache,psr/clock
Ports
| Interface | Contract |
|---|---|
Port\Config |
store settings, getHmacSecret() derived from the platform key |
Port\Request |
header, client address behind trusted proxies, URI with query string |
Port\CookieJar |
cookie read and write with Path=/, HttpOnly, Secure, SameSite=Lax |
Port\HttpClient |
form POST, body only for status 200, null in every other case |
Port\FilterCounter |
codes of filters active in the request, no empty values |
Psr\SimpleCache\CacheInterface |
rDNS result cache and circuit breaker counter |
Psr\Clock\ClockInterface |
cookie signature timestamp |
Psr\Log\LoggerInterface |
botshield_* events with context |
Classes
| Class | Role |
|---|---|
ChallengeGuard |
evaluate($context) returns true when the request must get the challenge |
ChallengePage |
widget page HTML, cache-blocking HEADERS, translatable TEXTS |
ChallengeResponse |
redirect() for pages, ajax() for scripts, afterVerification() for the verification endpoint |
ChallengeVerification |
Turnstile token → cookie, result as VerificationResult |
Resolver\AjaxDetector |
X-Requested-With, Sec-Fetch-Dest: empty, Accept: application/json |
Turnstile\Verifier |
token verification via siteverify, fail-open on network error |
Verification\Cookie |
read, validation and issuing of the ts_verified cookie |
Verification\Bot |
X-Verified-Bot header and rDNS with forward confirmation, DEFAULT_BOT_DOMAINS |
Resolver\ClientIp |
client address, /24 or /64 binding prefix, private address detection |
CookieSigner, ReturnUrl, Verification\IpRange |
HMAC signature, safe return URL, CIDR matching |
AJAX
Filters reloaded via AJAX get 403 instead of 302. The challenge opens as a modal on the current page.
AJAX request → ChallengeGuard::evaluate() === true, AjaxDetector::isAjax() === true
← ChallengeResponse::ajax(): 403, X-BotShield-Challenge: 1, {"challenge": {url, siteKey, verifyUrl, texts}}
assets/challenge-interceptor.js (wrapped fetch + XMLHttpRequest)
→ Turnstile modal → POST cf-turnstile-response, Accept: application/json
← ChallengeResponse::afterVerification(json: true): {"ok": true} + Set-Cookie ts_verified
→ location.reload()
| Platform responsibility | Detail |
|---|---|
include assets/challenge-interceptor.js on listings |
no configuration, data arrives in the 403 response |
build url with r pointing to the page |
not the AJAX endpoint, e.g. without from-xhr in PrestaShop |
add platform AJAX markers to AjaxDetector::isAjax() |
from-xhr parameter, admin-ajax action |
send Http\Response through the platform response object |
status, headers, body |
Page reload restores filters only when the filter plugin stores them in the URL via pushState.
Tests
composer install
vendor/bin/phpunit
vendor/bin/phpstan analyse
node --test tests/js/*.test.js
Verified on PHP 8.2.1 and 8.5: 184 tests, phpstan level 8 with no errors. Node 22.22: 9 interceptor tests. The Turnstile modal has no automated test.