This documentation always refers to the latest published release.
Live demo: Try this and other plugins live, no installation, no risk: demo.markus-michalski.net
All three protected forms run through the same SpamProtectionFacade. The order is sorted by cost: cheap in-memory checks first, the cryptographic ALTCHA check after that, the rate limit with database access last.
A layer that is switched off is skipped. Content validation is additionally skipped as soon as gibberish detection or the disposable e-mail check is on. The rate limit counts only submissions that passed all layers before it.
Registration is protected through a route decorator, not an event. The check therefore runs before the account is created.
Contact form and newsletter run through event subscribers on contact_form.send and NewsletterRegisterEvent (priority 500), using the same facade.
Gibberish detection scores every text field and takes the worst value. The score is the share of letters in conspicuous words, weighted by the amount of evidence:
| Signal | Description |
|---|---|
| Letter sequences | The trigrams of a word are implausible in every language of the bundled table (German, English, French, Spanish, Italian, Dutch) |
| Keyboard runs | Almost all letters of a word are neighbours in one keyboard row (asdfgh, qwertz) |
| Repetition | A word consists of a short repeated unit (asdfasdf) |
Skipped are URLs, e-mail addresses, code, words containing digits or symbols, and words in non-Latin scripts. Capitalised words count less (names), and short words rarely trigger a rejection on their own. A long lowercase word made of impossible letter sequences, however, can reach a high score by itself. The table is at src/Resources/data/gibberish-trigrams.php. If it is missing or corrupt, the check scores nothing and logs a single warning.
The deprecated content validation, in contrast, uses fixed rules (consonant ratio, keyboard sequences, repetition, consonant clusters, letter pairs) on first name, last name, street and city as well as the e-mail domain, and blocks from two conspicuous fields.
mmd_altcha_rate_limit_logStores individual timestamps for a sliding time window.
| Column | Type | Description |
|---|---|---|
id |
BINARY(16) |
Primary key |
ip_hash |
CHAR(64) |
HMAC-SHA256 hash of the IP address |
action |
VARCHAR(50) |
contact_form, newsletter, registration, challenge |
sales_channel_id |
BINARY(16) |
optional, foreign key to sales_channel |
requested_at |
DATETIME(3) |
Time of the request |
Indexes: idx_rate_check (ip_hash, action, requested_at), idx_rate_check_channel (adds sales_channel_id) and idx_cleanup (requested_at).
mmd_altcha_used_challengePrevents replay attacks.
| Column | Type | Description |
|---|---|---|
challenge_hash |
CHAR(64) |
Primary key, nonce of the challenge |
expires_at |
DATETIME(3) |
Expiry time |
created_at |
DATETIME(3) |
Creation time |
INSERT IGNORE claims a challenge atomically: only the first writer wins.
The disposable-domain list and the trigram table are files in the plugin or under var/mmd_altcha/, not in the database.
Blocked attempts appear in the Shopware log, never with the submitted text:
[INFO] Spam attempt blocked {"reason":"honeypot_triggered","ip_hash":"...","sales_channel_id":"..."}
[INFO] Registration spam attempt blocked {"reason":"disposable_email","ip_hash":"...","sales_channel_id":"..."}
reason |
Meaning |
|---|---|
honeypot_triggered |
Honeypot field was filled |
too_fast |
Submission faster than the minimum fill time |
disposable_email |
Disposable e-mail address detected |
gibberish |
Gibberish score reaches the reject threshold |
content_validation_failed |
Two or more fields conspicuous in the deprecated content validation |
altcha_missing |
No ALTCHA payload in the request |
altcha_failed |
ALTCHA solution invalid, expired or already used |
rate_limited |
IP rate limit exceeded |
APP_SECRET. If it is empty, verifying a solution fails closed (the submission is rejected). The challenge endpoint, in contrast, throws an exception and answers with HTTP 500, and outputting the token field (form_token_field) causes an error on the form page.hash_equals().MmdAltchaIntegration/
βββ Altcha/ # bundled altcha-org/altcha library (MIT)
βββ Command/UpdateDisposableDomainsCommand # mmd:altcha:update-disposable-domains
βββ Controller/Api/BlockedDomainsController # GET /api/_action/mmd-altcha/blocked-domains
βββ Core/
β βββ Checkout/Customer/SalesChannel/RegisterRouteDecorator
β βββ Content/RateLimitLog/ # entity definition, entity, collection
βββ DisposableEmail/ # checker, list reader, updater, catalog, parser
βββ Exception/ # SpamDetectedException, SpamRejection
βββ Gibberish/ # guard, scorer, score, TrigramModel
βββ Migration/ # rate-limit and used-challenge tables
βββ ScheduledTask/ # rate_limit_cleanup, disposable_domains_refresh
βββ Service/ # challenge, honeypot, form token, rate limit, facade
βββ Storefront/Controller/AltchaChallengeController
βββ Subscriber/ # ContactFormSubscriber, NewsletterSubscriber
βββ Twig/AltchaExtension # Twig functions for templates