* * 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 GuzzleHttp\Exception\ConnectException; 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 { /** * Twilio client */ protected Client $twilioClient; /** * TRUE if the client is properly configured */ protected bool $isConfigured = false; protected LoggerInterface $logger; protected CacheItemPoolInterface $cachePool; const LOOKUP_URI = 'https://lookups.twilio.com/v1/PhoneNumbers/%s'; const FORMAT_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']) && strlen($config['twilio_sid']) > 2 && \array_key_exists('twilio_secret', $config) && !empty($config['twilio_secret']) && strlen($config['twilio_secret']) > 2 ) { $this->twilioClient = new Client([ 'auth' => [ $config['twilio_sid'], $config['twilio_secret'] ] ]); $this->isConfigured = TRUE; } } /** * Return true if the validation is configured and available. * * @return bool */ public function isPhonenumberValidationConfigured() : bool { return $this->isConfigured; } /** * REturn true if the phoennumber is a mobile phone. Return always true * if the validation is not configured. * * @param string $phonenumber * @return bool */ public function isValidPhonenumberMobile($phonenumber) : bool { if (FALSE === $this->isPhonenumberValidationConfigured()) { return true; } $validation = $this->performTwilioLookup($phonenumber); if (NULL === $validation) { return true; } return $validation === 'mobile'; } /** * Return true if the phonenumber is a landline or voip phone. Return always true * if the validation is not configured. * * @param string $phonenumber * @return bool */ public function isValidPhonenumberLandOrVoip($phonenumber) : bool { if (FALSE === $this->isPhonenumberValidationConfigured()) { return true; } $validation = $this->performTwilioLookup($phonenumber); if (NULL === $validation) { return true; } return \in_array($validation, [ 'landline', 'voip' ]); } /** * Return true if the phonenumber is a landline or voip phone. Return always true * if the validation is not configured. * * @param string $phonenumber * @return bool */ public function isValidPhonenumberAny($phonenumber) : bool { if (FALSE === $this->isPhonenumberValidationConfigured()) { return true; } $validation = $this->performTwilioLookup($phonenumber); ; if (NULL === $validation) { return false; } return \in_array($validation, [ 'landline', 'voip', 'mobile' ]); } /** * Get type (mobile, landline, ...) for phone number * * @param string $phonenumber * * @return string */ public function getType(string $phonenumber): string { return $this->performTwilioLookup($phonenumber) ?? 'unknown'; } public function format($phonenumber) { return $this->performTwilioFormat($phonenumber); } protected function performTwilioFormat($phonenumber) { if (FALSE === $this->isPhonenumberValidationConfigured()) { return $phonenumber; } // filter only number $filtered = \preg_replace("/[^0-9]/", "", $phonenumber); $item = $this->cachePool->getItem('pnum_format_nat_'.$filtered); if ($item->isHit()) { return $item->get(); } try { $response = $this->twilioClient->get(sprintf(self::FORMAT_URI, '+'.$filtered), [ 'http_errors' => true, ]); } catch (ClientException $e) { $response = $e->getResponse(); $this->logger->error("[phonenumber helper] Could not format number " . "due to client error", [ "message" => $response->getBody()->getContents(), "status_code" => $response->getStatusCode(), "phonenumber" => $phonenumber ]); return $phonenumber; } catch (ServerException $e) { $response = $e->getResponse(); $this->logger->error("[phonenumber helper] Could not format number " . "due to server error", [ "message" => $response->getBody()->getContents(), "status_code" => $response->getStatusCode(), "phonenumber" => $phonenumber ]); return null; } catch (ConnectException $e) { $this->logger->error("[phonenumber helper] Could not format number " . "due to connect error", [ "message" => $e->getMessage(), "phonenumber" => $phonenumber ]); return null; } $format = \json_decode($response->getBody())->national_format; $item ->set($format) // expires after 3d ->expiresAfter(3600 * 24 * 3) ; $this->cachePool->save($item); return $format; } 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) { $response = $e->getResponse(); $this->logger->error("[phonenumber helper] Could not perform validation " . "due to server error", [ "message" => $response->getBody()->getContents(), "status_code" => $response->getStatusCode(), "phonenumber" => $phonenumber ]); return null; } catch (ConnectException $e) { $this->logger->error("[phonenumber helper] Could not format number " . "due to connect error", [ "message" => $e->getMessage(), "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; } }