ALTCHA Spam Protection: Technical Details
Technical details of the ALTCHA Spam Protection module for OXID 7: check chain, extended core classes, module structure, services and the two database tables with columns, actions and retention.
ALTCHA Spam Protection: Technical Details
Live Demo: Try this and other plugins live β no installation, no risk: demo.markus-michalski.net
Check chain
The class SpamProtectionFacade runs the checks in this order. The first failed check ends the chain:
graph TD
A["Submission"] --> B{"Spam protection enabled?"}
B -->|"No"| Z["Accepted"]
B -->|"Yes"| C{"Honeypot active and filled?"}
C -->|"Yes"| X1["Rejected: honeypot_triggered"]
C -->|"No"| D{"Content validation: 2 or more flagged fields?"}
D -->|"Yes"| X2["Rejected: content_validation_failed"]
D -->|"No"| E{"ALTCHA active and solution invalid?"}
E -->|"Yes"| X3["Rejected: altcha_missing, altcha_invalid, altcha_expired, altcha_replay"]
E -->|"No"| F{"Rate limit active and exceeded?"}
F -->|"Yes"| X4["Rejected: rate_limit_exceeded"]
F -->|"No"| Z
A submission has to pass all enabled checks. Content validation only runs when the form supplies evaluable fields. The ALTCHA solution is verified before the rate limit and marked as used in the process. The rate limit only counts submissions that passed the earlier checks.
Extended OXID classes
The extensions are registered in metadata.php under extend:
| OXID class | Module class | Purpose |
|---|---|---|
OxidEsales\Eshop\Core\ViewConfig |
Mmd\AltchaIntegration\Extension\Core\ViewConfig |
Helper methods for templates |
OxidEsales\Eshop\Application\Controller\ContactController |
Mmd\AltchaIntegration\Extension\Controller\ContactController |
Check before send() |
OxidEsales\Eshop\Application\Controller\NewsletterController |
Mmd\AltchaIntegration\Extension\Controller\NewsletterController |
Check before send() |
OxidEsales\Eshop\Application\Component\UserComponent |
Mmd\AltchaIntegration\Extension\Component\UserComponent |
Check before createUser(), only for cl=register |
In addition the module registers the controller mmd_altcha_challenge (Mmd\AltchaIntegration\Controller\AltchaChallengeController) and the template @mmd_altcha/widget/altcha_widget.
Module structure
oxid7-altcha-integration/
βββ metadata.php # module ID mmd_altcha, settings, events
βββ services.yaml # services (autowire, public)
βββ src/
β βββ Controller/ # AltchaChallengeController
β βββ Core/ # ModuleEvents (onActivate, onDeactivate)
β βββ Exception/ # three exception classes, not used anywhere in the module
β βββ Extension/ # extensions of the OXID classes
β βββ Repository/ # RateLimitRepository, UsedChallengeRepository
β βββ Service/ # check services and settings
β βββ Struct/ # SpamCheckRequest, SpamCheckResult, AltchaChallenge, AltchaSolution
βββ translations/ # frontend translations (de, en)
βββ views/
βββ admin_twig/ # admin translations (de, en)
βββ twig/ # widget and Apex overrides
Services
| Service | Purpose |
|---|---|
SpamProtectionFacade |
Runs the check chain |
HoneypotService |
Checks that the honeypot field is not empty |
ContentValidationService |
Gibberish and disposable domain checks |
AltchaChallengeService |
Creates challenges and verifies solutions |
RateLimitService |
Rate limit for forms |
ChallengeRateLimitService |
Rate limit for the challenge endpoint |
ModuleSettingsService |
Reads the settings, falling back to defaults |
RateLimitRepository, UsedChallengeRepository |
Database access |
If a setting cannot be read, ModuleSettingsService uses the default value.
Database tables
Activation creates both tables with CREATE TABLE IF NOT EXISTS (InnoDB, utf8mb4).
mmd_altcha_rate_limit_log
One row per request:
| Column | Type | Content |
|---|---|---|
id |
BIGINT UNSIGNED | Primary key, built from a microsecond timestamp and a random number |
ip_hash |
CHAR(64) | HMAC-SHA256 of ip: plus the client address, keyed with the HMAC secret |
action |
VARCHAR(50) | contact_form, newsletter_subscribe, registration or challenge |
requested_at |
DATETIME | Time of the request |
created_at |
DATETIME | Time of the entry |
Indexes: idx_rate_check (ip_hash, action, requested_at) and idx_cleanup (requested_at). The limit counts the rows with matching hash and action whose requested_at lies within the time window. Only when the count is below the maximum is a new row written.
mmd_altcha_used_challenges
| Column | Type | Content |
|---|---|---|
id |
BIGINT UNSIGNED | Primary key |
challenge_hash |
CHAR(64) | SHA-256 of the used challenge, unique |
expires_at |
DATETIME | Time of use plus "Challenge validity (seconds)" |
created_at |
DATETIME | Time of the entry |
The unique index on challenge_hash makes recording atomic: submitting the same challenge a second time adds no row and is rejected as a replay. The index idx_cleanup is on expires_at.
Retention
The module deletes no rows. The methods RateLimitService::cleanup(), RateLimitRepository::deleteOlderThan() and UsedChallengeRepository::deleteExpired() exist but are never called, there is neither a cron job nor a hook. Both tables grow without limit unless you clean them up yourself (instructions on the Privacy page).
Module events
| Event | Behaviour |
|---|---|
onActivate |
Creates the two tables |
onDeactivate |
Does nothing |
Translations
The frontend texts (widget labels and error messages, keys with the prefix MMD_ALTCHA_) are in translations/de/mmd_altcha_lang.php and translations/en/mmd_altcha_lang.php. The admin labels of the settings are in views/admin_twig/de/mmd_altcha_lang.php and views/admin_twig/en/mmd_altcha_lang.php.