tobento / app-spam
App spam protection.
Requires
- php: >=8.0
- psr/container: ^2.0
- tobento/app: ^1.0.7
- tobento/app-encryption: ^1.0
- tobento/app-http: ^1.0 || ^1.1
- tobento/app-migration: ^1.0
- tobento/app-view: ^1.0
- tobento/service-autowire: ^1.0
Requires (Dev)
- phpunit/phpunit: ^9.5
- tobento/app-event: ^1.0.1
- tobento/app-testing: ^1.0
- tobento/app-validation: ^1.0
- tobento/service-filesystem: ^1.0.5
- vimeo/psalm: ^4.0
Suggests
- tobento/app-validation: To support detecting spam using the validator
README
Spam protection for forms and detecting spam using the validator.
Table of Contents
- Getting Started
- Documentation
- Credits
Getting Started
Add the latest version of the app spam project running this command.
composer require tobento/app-spam
Requirements
- PHP 8.0 or greater
Documentation
App
Check out the App Skeleton if you are using the skeleton.
You may also check out the App to learn more about the app in general.
Spam Boot
The spam boot does the following:
- installs and loads spam config file
- implements spam interfaces
use Tobento\App\AppFactory; use Tobento\App\Spam\DetectorsInterface; // Create the app $app = (new AppFactory())->createApp(); // Add directories: $app->dirs() ->dir(realpath(__DIR__.'/../'), 'root') ->dir(realpath(__DIR__.'/../app/'), 'app') ->dir($app->dir('app').'config', 'config', group: 'config') ->dir($app->dir('root').'public', 'public') ->dir($app->dir('root').'vendor', 'vendor'); // Adding boots: $app->boot(\Tobento\App\Spam\Boot\Spam::class); $app->booting(); // Implemented interfaces: $detectors = $app->get(DetectorsInterface::class); // Run the app $app->run();
Spam Config
The configuration for the spam is located in the app/config/spam.php
file at the default App Skeleton config location where you can specify detectors for your application.
Basic Usage
Render Detector
In your view file, render the detector on your form using the spamDetector
view macro:
<form> <!-- Using the default --> <?= $view->spamDetector()->render($view) ?> <!-- Or using a specific detector --> <?= $view->spamDetector('register')->render($view) ?> </form>
Check out the App View to learn more about it.
Using a factory
Alternatively, you can specify a detector factory:
use Tobento\App\Spam\Factory; <form> <?= $view->spamDetector(new Factory\Honeypot(inputName: 'name'))->render($view) ?> </form>
Check out the Available Factories for its available detector factories.
Detect Spam
To protect your form against spam, add the ProtectAgainstSpam
middleware to the route that your form points to.
use Tobento\App\AppFactory; use Tobento\App\Spam\Middleware\ProtectAgainstSpam; $app = (new AppFactory())->createApp(); // Add directories: $app->dirs() ->dir(realpath(__DIR__.'/../'), 'root') ->dir(realpath(__DIR__.'/../app/'), 'app') ->dir($app->dir('app').'config', 'config', group: 'config') ->dir($app->dir('root').'public', 'public') ->dir($app->dir('root').'vendor', 'vendor'); // Adding boots: $app->boot(\Tobento\App\View\Boot\View::class); $app->boot(\Tobento\App\View\Boot\Form::class); $app->boot(\Tobento\App\Spam\Boot\Spam::class); $app->booting(); // Routes: $app->route('POST', 'register', function() { // being spam protected! return 'response'; })->middleware([ ProtectAgainstSpam::class, // you may specify a specific detector other than the default: 'detector' => 'register', // or you may specify a detector factory: 'detector' => new Factory\Composite( new Factory\Named('default'), new Factory\WithoutUrl(inputNames: ['message']), ), ]); // Run the app: $app->run();
Check out the Available Factories for its available detector factories.
Available Detectors
Composite Detector
The Composite
detector may be used to compose detectors:
use Tobento\App\Spam\Detector; $detector = new Detector\Composite( 'default', new Detector\Honeypot(name: 'default', inputName: 'hp'), );
EmailDomain Detector
The EmailDomain
detector, detects spam email domains from the the specified blacklist.
use Tobento\App\Spam\Detector; $detector = new Detector\EmailDomain( name: 'default', inputName: 'email', blacklist: ['mail.ru'], // Email domains considered as spam. whitelist: ['gmail.com'], // Email domains not considered as spam, exludes from blacklist. );
You may consider to create a factory to import blacklisted email domains from a file or any other source.
EmailRemote Detector
The EmailRemote
detector, detects spam email domains using PHP In-built functions getmxrr()
, checkdnsrr()
and fsockopen()
to verify email domain.
use Tobento\App\Spam\Detector; $detector = new Detector\EmailRemote( name: 'default', inputName: 'email', checkDNS: true, checkSMTP: true, checkMX: true, timeoutInSeconds: 5, );
Honeypot Detector
The Honeypot
detector, renders an invisible input element that should never contain a value when submitted. If a bot fills this input out, or removes the input from the request, the request will be detected as spam.
use Tobento\App\Spam\Detector; $detector = new Detector\Honeypot( name: 'default', inputName: 'hp', );
MinTimePassed Detector
The MinTimePassed
detector, renders an invisible input element with the time in it as an encrypted value. If the form is submitted faster than defined milliseconds
, or removes the input from the request, the request will be detected as spam.
use Psr\Clock\ClockInterface; use Tobento\App\Spam\Detector; use Tobento\Service\Encryption\EncrypterInterface; $detector = new Detector\MinTimePassed( encrypter: $encrypter, // EncrypterInterface clock: $clock, // ClockInterface name: 'default', inputName: 'mtp', milliseconds: 1000, );
You may check out the App Encryption to learn more about it.
Null Detector
The NullDetector
detector does not detect any request as spam at all.
use Tobento\App\Spam\Detector; $detector = new Detector\NullDetector(name: 'null');
WithoutUrl Detector
If the defined inputNames
contain an URL, the request will be detected as spam by the WithoutUrl
detector.
use Tobento\App\Spam\Detector; $detector = new Detector\WithoutUrl( name: 'default', inputNames: ['message'], );
Available Factories
Composite Factory
The Composite
factory creates a Composite Detector:
use Tobento\App\Spam\Factory; $factory = new Factory\Composite( new Factory\Honeypot(), );
EmailRemote Factory
The EmailRemote
factory creates a EmailRemote Detector:
use Tobento\App\Spam\Factory; $factory = new Factory\EmailRemote( inputName: 'email', checkDNS: true, checkSMTP: true, checkMX: true, timeoutInSeconds: 5, );
Honeypot Factory
The Honeypot
factory creates a Honeypot Detector:
use Tobento\App\Spam\Factory; $factory = new Factory\Honeypot( // you may change the default input name: inputName: 'hp', );
MinTimePassed Factory
The MinTimePassed
factory creates a MinTimePassed Detector:
use Tobento\App\Spam\Factory; $factory = new Factory\MinTimePassed( // you may change the default input name: inputName: 'mtp', // you may change the default input name: milliseconds: 1000, // you may change the enrypter to be used, otherwise the default is used: encrypterName: 'spam', );
Named Factory
The Named
factory may be used to create a detector from a named detector:
use Tobento\App\Spam\Factory; $factory = new Factory\Named( detector: 'default', );
Check out the Register Named Detectors section to learn more about it.
WithoutUrl Factory
The WithoutUrl
factory creates a WithoutUrl Detector:
use Tobento\App\Spam\Factory; $factory = new Factory\WithoutUrl( inputNames: ['message'], );
Register Named Detectors
Register Named Detector via Config
You can register named detectors in the config file app/config/spam.php
:
use Tobento\App\Spam\Detector; use Tobento\App\Spam\DetectorInterface; use Tobento\App\Spam\Factory; return [ // ... 'detectors' => [ // using a factory: 'default' => new Factory\Composite( new Factory\Honeypot(inputName: 'hp'), new Factory\MinTime(milliseconds: 1000), ), // using a closure: 'secondary' => static function (string $name): DetectorInterface { return new Detector\Composite( new Detector\Honeypot(name: $name, inputName: 'hp'), ); }, // using a class instance: 'null' => new Detector\NullDetector(name: 'null'), ], ];
Register Named Detector via Boot
use Tobento\App\Boot; use Tobento\App\Spam\Boot\Spam; use Tobento\App\Spam\Detector; use Tobento\App\Spam\DetectorFactoryInterface; use Tobento\App\Spam\DetectorsInterface; use Tobento\App\Spam\Factory; class SpamDetectorsBoot extends Boot { public const BOOT = [ // you may ensure the spam boot. Spam::class, ]; public function boot() { // you may use the app on method to add only if requested: $app->on( DetectorsInterface::class, static function(DetectorsInterface $detectors) { $detectors->add( name: 'null', detector: new Detector\NullDetector(), // DetectorInterface|DetectorFactoryInterface ); } ); } }
Manually Detecting Spam
After having booted the spam, inject the DetectorsInterface
in any service or controller.
use Psr\Http\Message\ServerRequestInterface; use Tobento\App\Spam\DetectorsInterface; use Tobento\App\Spam\Exception\SpamDetectedException; class SpamService { public function isSpam(ServerRequestInterface $request, DetectorsInterface $detectors): bool { try { $detectors->get('name')->detect($request); } catch (SpamDetectedException $e) { return true; } return false; } }
Detecting spam from value
use Psr\Http\Message\ServerRequestInterface; use Tobento\App\Spam\DetectorsInterface; use Tobento\App\Spam\Exception\SpamDetectedException; class SpamService { public function isSpam(mixed $value, DetectorsInterface $detectors): bool { try { $detectors->get('name')->detectFromValue($value); } catch (SpamDetectedException $e) { return true; } return false; } }
Detecting Spam Using Validator
Requirements
It requires the App Validation:
composer require tobento/app-validation
In addition, you may boot the ValidationSpamRule
boot if you want to support string definition rule like spam:detector_name
.
use Tobento\App\AppFactory; // Create the app $app = (new AppFactory())->createApp(); // Adding boots $app->boot(\Tobento\App\Spam\Boot\ValidationSpamRule::class); $app->boot(\Tobento\App\Spam\Boot\Spam::class); // Run the app $app->run();
Otherwise, you will need to boot the validator boot:
use Tobento\App\AppFactory; // Create the app $app = (new AppFactory())->createApp(); // Adding boots $app->boot(\Tobento\App\Validation\Boot\Validator::class); $app->boot(\Tobento\App\Spam\Boot\Spam::class); // Run the app $app->run();
Spam Rule
use Tobento\App\Spam\Factory; use Tobento\App\Spam\Validation\SpamRule; $validation = $validator->validating( value: 'foo@example.com', rules: [ // using a detector name: new SpamRule( detector: 'email', // you may specify a custom error message: errorMessage: 'Custom error message', ), // using a detector factory: new SpamRule(detector: new Factory\Named('email')), // or if booted the ValidationSpamRule::class: 'spam:email', // or with multiple detector names: 'spam:emailRemote:emailDomain', ], );
Skip validation
You may use the skipValidation parameter in order to skip validation under certain conditions:
$validation = $validator->validating( value: 'foo', rules: [ // skips validation: new SpamRule(detector: 'email', skipValidation: true), // does not skip validation: new SpamRule(detector: 'email', skipValidation: false), // skips validation: new SpamRule(detector: 'email', skipValidation: fn (mixed $value): bool => $value === 'foo'), ], );
Http Spam Error Handler Boot
The http error handler boot does the following:
- handles
Tobento\App\Spam\Exception\SpamDetectedException::class
exceptions.
The boot is automatically loaded by the Spam Boot.
use Tobento\App\AppFactory; // Create the app $app = (new AppFactory())->createApp(); // Adding boots // $app->boot(\Tobento\App\Spam\Boot\HttpSpamErrorHandler::class); // not needed to boot! $app->boot(\Tobento\App\Spam\Boot\Spam::class); // Run the app $app->run();
The error handler will return a 422 Unprocessable Entity HTTP response if spam was detected.
You may create a custom Error Handler With A Higher Priority of 3000
as defined on the Tobento\App\Spam\Boot\HttpSpamErrorHandler::class
to handle spam exceptions to fit your application.
Events
Available Events
Supporting Events
Simply, install the App Event bundle.