From af5375fc382e6850fb9fcc276705e2686ff1f523 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 20 Aug 2018 21:33:01 +0200 Subject: [PATCH] Adding phonenumber validation constraint --- DependencyInjection/ChillMainExtension.php | 4 + DependencyInjection/Configuration.php | 11 ++ Phonenumber/PhonenumberHelper.php | 171 ++++++++++++++++++ Resources/config/services/phonenumber.yml | 13 ++ Resources/translations/validators.fr.yml | 6 +- .../Constraint/PhonenumberConstraint.php | 43 +++++ Validation/Validator/ValidPhonenumber.php | 83 +++++++++ 7 files changed, 330 insertions(+), 1 deletion(-) create mode 100644 Phonenumber/PhonenumberHelper.php create mode 100644 Resources/config/services/phonenumber.yml create mode 100644 Validation/Constraint/PhonenumberConstraint.php create mode 100644 Validation/Validator/ValidPhonenumber.php diff --git a/DependencyInjection/ChillMainExtension.php b/DependencyInjection/ChillMainExtension.php index be61a3d49..c2e6f58e5 100644 --- a/DependencyInjection/ChillMainExtension.php +++ b/DependencyInjection/ChillMainExtension.php @@ -85,6 +85,9 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface, $container->setParameter('chill_main.redis', $config['redis']); + $container->setParameter('chill_main.phone_helper', + $config['phone_helper'] ?? []); + // add the key 'widget' without the key 'enable' $container->setParameter('chill_main.widgets', isset($config['widgets']['homepage']) ? @@ -109,6 +112,7 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface, $loader->load('services/notification.yml'); $loader->load('services/redis.yml'); $loader->load('services/command.yml'); + $loader->load('services/phonenumber.yml'); } public function getConfiguration(array $config, ContainerBuilder $container) diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 6971ff9eb..7909bbd01 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -85,6 +85,17 @@ class Configuration implements ConfigurationInterface ->end() ->end() ->end() // end of notifications + ->arrayNode('phone_helper') + ->canBeUnset() + ->children() + ->scalarNode('twilio_sid') + ->defaultNull() + ->end() + ->scalarNode('twilio_secret') + ->defaultNull() + ->end() + ->end() + ->end() ->arrayNode('redis') ->children() ->scalarNode('host') diff --git a/Phonenumber/PhonenumberHelper.php b/Phonenumber/PhonenumberHelper.php new file mode 100644 index 000000000..772b43945 --- /dev/null +++ b/Phonenumber/PhonenumberHelper.php @@ -0,0 +1,171 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace Chill\MainBundle\Phonenumber; + +use GuzzleHttp\Client; +use GuzzleHttp\Exception\ClientException; +use GuzzleHttp\Exception\ServerException; +use Psr\Log\LoggerInterface; +use Psr\Cache\CacheItemPoolInterface; + +/** + * Helper to some task linked to phonenumber. + * + * Currently, only Twilio is supported (https://www.twilio.com/lookup). A method + * allow to check if the helper is configured for validation. This should be used + * before doing some validation. + * + * + */ +class PhonenumberHelper +{ + /** + * + * @var Client + */ + protected $twilioClient; + + /** + * + * @var LoggerInterface + */ + protected $logger; + + /** + * + * @var CacheItemPoolInterface + */ + protected $cachePool; + + const LOOKUP_URI = 'https://lookups.twilio.com/v1/PhoneNumbers/%s'; + + + public function __construct( + CacheItemPoolInterface $cachePool, + $config, + LoggerInterface $logger + ) { + $this->logger = $logger; + $this->cachePool = $cachePool; + + if (\array_key_exists('twilio_sid', $config) + && !empty($config['twilio_sid']) + && \array_key_exists('twilio_secret', $config) + && !empty($config['twilio_secret'])) { + + $this->twilioClient = new Client([ + 'auth' => [ $config['twilio_sid'], $config['twilio_secret'] ] + ]); + } + + } + + /** + * Return true if the validation is configured and available. + * + * @return bool + */ + public function isPhonenumberValidationConfigured() : bool + { + return NULL !== $this->twilioClient; + } + + /** + * REturn true if the phoennumber is a mobile phone. Return always false + * if the validation is not configured. + * + * @param string $phonenumber + * @return bool + */ + public function isValidPhonenumberMobile($phonenumber) : bool + { + $validation = $this->performTwilioLookup($phonenumber); + + if (NULL === $validation) { + return false; + } + + return $validation === 'mobile'; + } + + /** + * Return true if the phonenumber is a landline or voip phone. Return always false + * if the validation is not configured. + * + * @param string $phonenumber + * @return bool + */ + public function isValidPhonenumberLandOrVoip($phonenumber) : bool + { + $validation = $this->performTwilioLookup($phonenumber); + + if (NULL === $validation) { + return false; + } + + return \in_array($validation, [ 'landline', 'voip' ]); + } + + protected function performTwilioLookup($phonenumber) + { + if (FALSE === $this->isPhonenumberValidationConfigured()) { + return null; + } + + // filter only number + $filtered = \preg_replace("/[^0-9]/", "", $phonenumber); + + $item = $this->cachePool->getItem('pnum_'.$filtered); + + if ($item->isHit()) { + return $item->get(); + } + + try { + $response = $this->twilioClient->get(sprintf(self::LOOKUP_URI, '+'.$filtered), [ + 'http_errors' => true, + 'query' => [ + 'Type' => 'carrier' + ] + ]); + } catch (ClientException $e) { + return 'invalid'; + } catch (ServerException $e) { + $this->logger->error("[phonenumber helper] Could not perform validation " + . "due to server error", [ + "message" => $e->getResponseBodySummary($e->getResponse()), + "status_code" => $e->getResponse()->getStatusCode(), + "phonenumber" => $phonenumber + ]); + + return null; + } + + $validation = \json_decode($response->getBody())->carrier->type; + + $item + ->set($validation) + // expires after 12h + ->expiresAfter(3600 * 12) + ; + + $this->cachePool->save($item); + + return $validation; + } +} diff --git a/Resources/config/services/phonenumber.yml b/Resources/config/services/phonenumber.yml new file mode 100644 index 000000000..ec519c836 --- /dev/null +++ b/Resources/config/services/phonenumber.yml @@ -0,0 +1,13 @@ +services: + Chill\MainBundle\Phonenumber\PhonenumberHelper: + arguments: + $logger: '@Psr\Log\LoggerInterface' + $config: '%chill_main.phone_helper%' + $cachePool: '@cache.user_data' + + Chill\MainBundle\Validation\Validator\ValidPhonenumber: + arguments: + $logger: '@Psr\Log\LoggerInterface' + $phonenumberHelper: '@Chill\MainBundle\Phonenumber\PhonenumberHelper' + tags: + - { name: validator.constraint_validator } diff --git a/Resources/translations/validators.fr.yml b/Resources/translations/validators.fr.yml index db3c2e0ab..ab3596c79 100644 --- a/Resources/translations/validators.fr.yml +++ b/Resources/translations/validators.fr.yml @@ -12,4 +12,8 @@ A permission is already present for the same role and scope: Une permission est "{{ username }} is not allowed to see entities published in this circle": "{{ username }} n'est pas autorisé à voir l'élément publié dans ce cercle." #password request -This username or email does not exists: Cet utilisateur ou email n'est pas présent dans la base de donnée \ No newline at end of file +This username or email does not exists: Cet utilisateur ou email n'est pas présent dans la base de donnée + +#phonenumber +This is not a landline phonenumber: Ce numéro n'est pas une ligne fixe valide +This is not a mobile phonenumber: Ce numéro n'est pas un numéro de portable valide diff --git a/Validation/Constraint/PhonenumberConstraint.php b/Validation/Constraint/PhonenumberConstraint.php new file mode 100644 index 000000000..9e322285d --- /dev/null +++ b/Validation/Constraint/PhonenumberConstraint.php @@ -0,0 +1,43 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace Chill\MainBundle\Validation\Constraint; + +use Symfony\Component\Validator\Constraint; + +/** + * + * + */ +class PhonenumberConstraint extends Constraint +{ + public $notMobileMessage = "This is not a mobile phonenumber"; + + public $notLandlineMessage = "This is not a landline phonenumber"; + + /** + * The type of phone: landline (not able to receive sms) or mobile (can receive sms) + * + * @var string 'landline' or 'mobile' + */ + public $type = null; + + public function validatedBy() + { + return \Chill\MainBundle\Validation\Validator\ValidPhonenumber::class; + } +} diff --git a/Validation/Validator/ValidPhonenumber.php b/Validation/Validator/ValidPhonenumber.php new file mode 100644 index 000000000..971dedc25 --- /dev/null +++ b/Validation/Validator/ValidPhonenumber.php @@ -0,0 +1,83 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace Chill\MainBundle\Validation\Validator; + +use Symfony\Component\Validator\ConstraintValidator; +use Symfony\Component\Validator\Constraint; +use Chill\MainBundle\Phonenumber\PhonenumberHelper; +use Psr\Log\LoggerInterface; + +/** + * + * + */ +class ValidPhonenumber extends ConstraintValidator +{ + /** + * + * @var PhonenumberHelper + */ + protected $phonenumberHelper; + + protected $logger; + + public function __construct( + LoggerInterface $logger, + PhonenumberHelper $phonenumberHelper + ) { + $this->phonenumberHelper = $phonenumberHelper; + $this->logger = $logger; + } + + /** + * + * @param string $value + * @param \Chill\MainBundle\Validation\Constraint\PhonenumberConstraint $constraint + */ + public function validate($value, Constraint $constraint) + { + if (FALSE === $this->phonenumberHelper->isPhonenumberValidationConfigured()) { + $this->logger->debug('[phonenumber] skipping validation due to not configured helper'); + + return; + } + + if (empty($value)) { + return; + } + + switch($constraint->type) { + case 'landline': + $isValid = $this->phonenumberHelper->isValidPhonenumberLandOrVoip($value); + $message = $constraint->notLandlineMessage; + break; + case 'mobile': + $isValid = $this->phonenumberHelper->isValidPhonenumberMobile($value); + $message = $constraint->notMobileMessage; + break; + + default: + throw new \LogicException(sprintf("This type '%s' is not implemented. " + . "Possible values are 'mobile', 'landline'"), $constraint->type); + } + + if (FALSE === $isValid) { + $this->context->addViolation($message, [ '%phonenumber%' => $value ]); + } + } +}