diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 98b4c64..444cbc9 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -44,6 +44,17 @@ public function getConfigTreeBuilder() ->defaultValue('Form fields are invalid')->end() ->end() ->end() + + + ->arrayNode('cookie') + ->canBeDisabled() + ->children() + ->scalarNode('name')->defaultValue('antispam')->end() + ->booleanNode('global')->defaultFalse()->end() + ->scalarNode('message') + ->defaultValue('Something is wrong, please try again')->end() + ->end() + ->end() ->end(); return $treeBuilder; diff --git a/DependencyInjection/IsometriksSpamExtension.php b/DependencyInjection/IsometriksSpamExtension.php index b2ad811..3783608 100644 --- a/DependencyInjection/IsometriksSpamExtension.php +++ b/DependencyInjection/IsometriksSpamExtension.php @@ -25,6 +25,7 @@ public function load(array $configs, ContainerBuilder $container) $this->processTimedConfig($config['timed'], $container, $loader); $this->processHoneypotConfig($config['honeypot'], $container, $loader); + $this->processCookieConfig($config['cookie'], $container, $loader); } private function processTimedConfig(array $config, ContainerBuilder $container, XmlFileLoader $loader) @@ -61,4 +62,20 @@ private function processHoneypotConfig(array $config, ContainerBuilder $containe 'message' => $config['message'], )); } + + private function processCookieConfig(array $config, ContainerBuilder $container, XmlFileLoader $loader) + { + if (!$this->isConfigEnabled($container, $config)) { + return; + } + + $loader->load('cookie.xml'); + + $definition = $container->getDefinition('isometriks_spam.form.extension.type.cookie'); + $definition->addArgument(array( + 'name' => $config['name'], + 'global' => $config['global'], + 'message' => $config['message'], + )); + } } diff --git a/EventListener/KernelResponseListener.php b/EventListener/KernelResponseListener.php new file mode 100644 index 0000000..df2a531 --- /dev/null +++ b/EventListener/KernelResponseListener.php @@ -0,0 +1,35 @@ +cookieProvider = $cookieProvider; + } + + public function onKernelResponse(FilterResponseEvent $responseEvent) + { + $cookieData = $this->cookieProvider->getCookieSettings(); + if ($cookieData) { + if($cookieData['mode'] == 'add') { + $cookie = new Cookie($cookieData['name'], 1); + + $responseEvent->getResponse()->headers->setCookie($cookie); + } else if ($cookieData['mode'] == 'remove') { + $responseEvent->getResponse()->headers->removeCookie($cookieData['name']); + $responseEvent->getResponse()->headers->clearCookie($cookieData['name']); + $this->cookieProvider->removeCookieSettings(); + } + } + } +} \ No newline at end of file diff --git a/Form/Extension/Spam/EventListener/CookieValidationListener.php b/Form/Extension/Spam/EventListener/CookieValidationListener.php new file mode 100644 index 0000000..8fc59bc --- /dev/null +++ b/Form/Extension/Spam/EventListener/CookieValidationListener.php @@ -0,0 +1,64 @@ +cookieProvider = $cookieProvider; + $this->request = $request; + $this->translator = $translator; + $this->translationDomain = $translationDomain; + $this->cookieName = $cookieName; + $this->errorMessage = $errorMessage; + } + + public function preSubmit(FormEvent $event) + { + $form = $event->getForm(); + + if($form->isRoot() && $form->getConfig()->getOption('compound') && !$this->request->cookies->get($this->cookieName)) { + $errorMessage = $this->errorMessage; + + if (null !== $this->translator) { + $errorMessage = $this->translator->trans($errorMessage, array(), $this->translationDomain); + } + + $form->addError(new FormError($errorMessage)); + + return; + } + + $this->cookieProvider->removeAntispamCookie($this->cookieName); + } + + + public static function getSubscribedEvents() + { + return array( + FormEvents::PRE_SUBMIT => 'preSubmit', + ); + } +} \ No newline at end of file diff --git a/Form/Extension/Spam/Provider/CookieProvider.php b/Form/Extension/Spam/Provider/CookieProvider.php new file mode 100644 index 0000000..01232cc --- /dev/null +++ b/Form/Extension/Spam/Provider/CookieProvider.php @@ -0,0 +1,36 @@ +session = $session; + } + + public function setAntispamCookie($cookieName) + { + $this->session->set('antispam_cookie', array('mode' => 'add', 'name' => $cookieName)); + } + + public function removeAntispamCookie($cookieName) + { + $this->session->set('antispam_cookie', array('mode' => 'remove', 'name' => $cookieName)); + } + + public function getCookieSettings() + { + return $this->session->get('antispam_cookie'); + } + + public function removeCookieSettings() + { + $this->session->remove('antispam_cookie'); + } +} \ No newline at end of file diff --git a/Form/Extension/Spam/Type/FormTypeCookieExtension.php b/Form/Extension/Spam/Type/FormTypeCookieExtension.php new file mode 100644 index 0000000..12ea1cf --- /dev/null +++ b/Form/Extension/Spam/Type/FormTypeCookieExtension.php @@ -0,0 +1,83 @@ +cookieProvider = $cookieProvider; + $this->request = $requestStack->getCurrentRequest(); + $this->session = $session; + $this->translator = $translator; + $this->translationDomain = $translationDomain; + $this->defaults = $defaults; + } + + public function buildForm(FormBuilderInterface $builder, array $options) + { + if (!$options['cookie']) { + return; + } + + $builder + ->addEventSubscriber(new CookieValidationListener( + $this->cookieProvider, + $this->request, + $this->translator, + $this->translationDomain, + $options['cookie_name'], + $options['cookie_message'] + )); + } + + public function finishView(FormView $view, FormInterface $form, array $options) + { + $this->cookieProvider->setAntispamCookie($options['cookie_name']); + } + + public function setDefaultOptions(OptionsResolver $resolver) + { + $this->configureOptions($resolver); + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults(array( + 'cookie' => $this->defaults['global'], + 'cookie_name' => $this->defaults['name'], + 'cookie_message' => $this->defaults['message'], + )); + } + + public function getExtendedType() + { + return FormType::class; + } +} \ No newline at end of file diff --git a/Form/Extension/Spam/Type/FormTypeHoneypotExtension.php b/Form/Extension/Spam/Type/FormTypeHoneypotExtension.php index 378eab1..1318171 100644 --- a/Form/Extension/Spam/Type/FormTypeHoneypotExtension.php +++ b/Form/Extension/Spam/Type/FormTypeHoneypotExtension.php @@ -10,7 +10,6 @@ use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormView; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\OptionsResolver\OptionsResolverInterface; use Symfony\Component\Translation\TranslatorInterface; class FormTypeHoneypotExtension extends AbstractTypeExtension @@ -19,10 +18,11 @@ class FormTypeHoneypotExtension extends AbstractTypeExtension private $translationDomain; private $defaults; - public function __construct(TranslatorInterface $translator, - $translationDomain, - array $defaults) - { + public function __construct( + TranslatorInterface $translator, + $translationDomain, + array $defaults + ) { $this->translator = $translator; $this->translationDomain = $translationDomain; $this->defaults = $defaults; @@ -74,7 +74,7 @@ public function finishView(FormView $view, FormInterface $form, array $options) } } - public function setDefaultOptions(OptionsResolverInterface $resolver) + public function setDefaultOptions(OptionsResolver $resolver) { $this->configureOptions($resolver); } diff --git a/Form/Extension/Spam/Type/FormTypeTimedSpamExtension.php b/Form/Extension/Spam/Type/FormTypeTimedSpamExtension.php index 05eeb11..92b73bd 100644 --- a/Form/Extension/Spam/Type/FormTypeTimedSpamExtension.php +++ b/Form/Extension/Spam/Type/FormTypeTimedSpamExtension.php @@ -20,11 +20,12 @@ class FormTypeTimedSpamExtension extends AbstractTypeExtension private $translationDomain; private $defaults; - public function __construct(TimedSpamProviderInterface $timeProvider, - TranslatorInterface $translator, - $translationDomain, - array $defaults) - { + public function __construct( + TimedSpamProviderInterface $timeProvider, + TranslatorInterface $translator, + $translationDomain, + array $defaults + ) { $this->timeProvider = $timeProvider; $this->translator = $translator; $this->translationDomain = $translationDomain; @@ -59,7 +60,7 @@ public function finishView(FormView $view, FormInterface $form, array $options) } } - public function setDefaultOptions(OptionsResolverInterface $resolver) + public function setDefaultOptions(OptionsResolver $resolver) { $this->configureOptions($resolver); } diff --git a/README.md b/README.md index e318022..7828896 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ If the field is filled out, then the form is invalid. You can optionally choose to use a class name to hide the form element as well in case the bot tries to check the style attribute. -```yml +```YAML isometriks_spam: honeypot: field: email_address @@ -123,9 +123,38 @@ public function configureOptions(OptionsResolver $resolver) } ``` -### Twig Form Error message rendering +### Cookie Spam Prevention + +Most of spam bots can't use cookies so we could check cookie value on each request. + +If there is no cookie set - then the request is invalid and set by bots. +These cookies are not used for any tracking and are deleted after form is submitted and handled. + +Configuration: +```YAML +isometriks_spam: + cookie: + name: antispam_cookie + global: false + message: Something is wrong, please try again ``` + +Usage: + +```php +public function configureOptions(OptionsResolver $resolver) +{ + $resolver->setDefaults(array( + 'cookie' => true, + // ... + )); +} +``` + +### Twig Form Error message rendering + +```twig {% if form.vars.errors is not empty %}