Markus Michalski FAQ

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.

Deutsche Version

Back to overview

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.