diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php index 23a9a47936df5..93a441bf0d27e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php @@ -22,6 +22,7 @@ use Symfony\Component\Notifier\Bridge\GoogleChat\GoogleChatTransportFactory; use Symfony\Component\Notifier\Bridge\Infobip\InfobipTransportFactory; use Symfony\Component\Notifier\Bridge\Iqsms\IqsmsTransportFactory; +use Symfony\Component\Notifier\Bridge\LightSms\LightSmsTransportFactory; use Symfony\Component\Notifier\Bridge\LinkedIn\LinkedInTransportFactory; use Symfony\Component\Notifier\Bridge\Mattermost\MattermostTransportFactory; use Symfony\Component\Notifier\Bridge\Mercure\MercureTransportFactory; @@ -159,5 +160,9 @@ ->parent('notifier.transport_factory.abstract') ->tag('chatter.transport_factory') ->tag('texter.transport_factory') + + ->set('notifier.transport_factory.lightsms', LightSmsTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') ; }; diff --git a/src/Symfony/Component/Notifier/Bridge/LightSms/.gitattributes b/src/Symfony/Component/Notifier/Bridge/LightSms/.gitattributes new file mode 100644 index 0000000000000..84c7add058fb5 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/LightSms/.gitattributes @@ -0,0 +1,4 @@ +/Tests export-ignore +/phpunit.xml.dist export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore diff --git a/src/Symfony/Component/Notifier/Bridge/LightSms/.gitignore b/src/Symfony/Component/Notifier/Bridge/LightSms/.gitignore new file mode 100644 index 0000000000000..c49a5d8df5c65 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/LightSms/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/src/Symfony/Component/Notifier/Bridge/LightSms/LICENSE b/src/Symfony/Component/Notifier/Bridge/LightSms/LICENSE new file mode 100644 index 0000000000000..efb17f98e7dd3 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/LightSms/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2021 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/Symfony/Component/Notifier/Bridge/LightSms/LightSmsTransport.php b/src/Symfony/Component/Notifier/Bridge/LightSms/LightSmsTransport.php new file mode 100644 index 0000000000000..e2d2d60dacd8e --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/LightSms/LightSmsTransport.php @@ -0,0 +1,179 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\LightSms; + +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Notifier\Exception\TransportException; +use Symfony\Component\Notifier\Exception\UnsupportedMessageTypeException; +use Symfony\Component\Notifier\Message\MessageInterface; +use Symfony\Component\Notifier\Message\SentMessage; +use Symfony\Component\Notifier\Message\SmsMessage; +use Symfony\Component\Notifier\Transport\AbstractTransport; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +/** + * @author Vasilij Duško + */ +final class LightSmsTransport extends AbstractTransport +{ + protected const HOST = 'www.lightsms.com'; + + private $login; + private $password; + private $from; + + private const ERROR_CODES = [ + 1 => 'Missing Signature', + 2 => 'Login not specified', + 3 => 'Text not specified', + 4 => 'Phone number not specified', + 5 => 'Sender not specified', + 6 => 'Invalid signature', + 7 => 'Invalid login', + 8 => 'Invalid sender name', + 9 => 'Sender name not registered', + 10 => 'Sender name not approved', + 11 => 'There are forbidden words in the text', + 12 => 'Error in SMS sending', + 13 => 'Phone number is in the blackist. SMS sending to this number is forbidden.', + 14 => 'There are more than 50 numbers in the request', + 15 => 'List not specified', + 16 => 'Invalid phone number', + 17 => 'SMS ID not specified', + 18 => 'Status not obtained', + 19 => 'Empty response', + 20 => 'The number already exists', + 21 => 'No name', + 22 => 'Template already exists', + 23 => 'Missing Month (Format: YYYY-MM)', + 24 => 'Timestamp not specified', + 25 => 'Error in access to the list', + 26 => 'There are no numbers in the list', + 27 => 'No valid numbers', + 28 => 'Missing start date (Format: YYYY-MM-DD)', + 29 => 'Missing end date (Format: YYYY-MM-DD)', + 30 => 'No date (format: YYYY-MM-DD)', + 31 => 'Closing direction to the user', + 32 => 'Not enough money', + 33 => 'Missing phone number', + 34 => 'Phone is in stop list', + 35 => 'Not enough money', + 36 => 'Can not obtain information about phone', + 37 => 'Base Id is not set', + 38 => 'Phone number already exists in this database', + 39 => 'Phone number does not exist in this database', + 999 => 'Unknown Error', + ]; + + public function __construct(string $login, string $password, string $from, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null) + { + $this->login = $login; + $this->password = $password; + $this->from = $from; + + parent::__construct($client, $dispatcher); + } + + public function __toString(): string + { + return sprintf('lightsms://%s?from=%s', $this->getEndpoint(), $this->from); + } + + public function supports(MessageInterface $message): bool + { + return $message instanceof SmsMessage; + } + + protected function doSend(MessageInterface $message): SentMessage + { + if (!$message instanceof SmsMessage) { + throw new UnsupportedMessageTypeException(__CLASS__, SmsMessage::class, $message); + } + + $timestamp = time(); + $data = [ + 'login' => $this->login, + 'phone' => $this->escapePhoneNumber($message->getPhone()), + 'text' => $message->getSubject(), + 'sender' => $this->from, + 'timestamp' => $timestamp, + ]; + $data['signature'] = $this->generateSignature($data, $timestamp); + + $endpoint = sprintf('https://%s/external/get/send.php', $this->getEndpoint()); + $response = $this->client->request( + 'GET', + $endpoint, + [ + 'query' => $data, + ] + ); + + if (Response::HTTP_OK !== $response->getStatusCode()) { + throw new TransportException('Unable to send the SMS.', $response); + } + + $content = $response->toArray(false); + + // it happens if the host without www + if (isset($content['']['error'])) { + throw new TransportException('Unable to send the SMS: '.$this->getErrorMsg((int) $content['']['error']), $response); + } + + if (isset($content['error'])) { + throw new TransportException('Unable to send the SMS: '.$this->getErrorMsg((int) $content['error']), $response); + } + + $phone = $this->escapePhoneNumber($message->getPhone()); + if (32 === (int) $content[$phone]['error']) { + throw new TransportException('Unable to send the SMS: '.$this->getErrorMsg((int) $content[$phone]['error']), $response); + } + + if (0 === (int) $content[$phone]['error']) { + $sentMessage = new SentMessage($message, (string) $this); + if (isset($content[$phone]['id_sms'])) { + $sentMessage->setMessageId($content[$phone]['id_sms']); + } + + return $sentMessage; + } + + throw new TransportException('Unable to send the SMS.', $response); + } + + private function generateSignature(array $data, int $timestamp): string + { + $params = [ + 'timestamp' => $timestamp, + 'login' => $this->login, + 'phone' => $data['phone'], + 'sender' => $this->from, + 'text' => $data['text'], + ]; + + ksort($params); + reset($params); + + return md5(implode('', $params).$this->password); + } + + private function escapePhoneNumber(string $phoneNumber): string + { + return str_replace('+', '00', $phoneNumber); + } + + private function getErrorMsg(int $errorCode): string + { + return self::ERROR_CODES[$errorCode] ?? self::ERROR_CODES[999]; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/LightSms/LightSmsTransportFactory.php b/src/Symfony/Component/Notifier/Bridge/LightSms/LightSmsTransportFactory.php new file mode 100644 index 0000000000000..9a7d172ca8aab --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/LightSms/LightSmsTransportFactory.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\LightSms; + +use Symfony\Component\Notifier\Exception\UnsupportedSchemeException; +use Symfony\Component\Notifier\Transport\AbstractTransportFactory; +use Symfony\Component\Notifier\Transport\Dsn; +use Symfony\Component\Notifier\Transport\TransportInterface; + +/** + * @author Vasilij Duško + */ +final class LightSmsTransportFactory extends AbstractTransportFactory +{ + /** + * @return LightSmsTransport + */ + public function create(Dsn $dsn): TransportInterface + { + $scheme = $dsn->getScheme(); + + if ('lightsms' !== $scheme) { + throw new UnsupportedSchemeException($dsn, 'lightsms', $this->getSupportedSchemes()); + } + + $login = $this->getUser($dsn); + $token = $this->getPassword($dsn); + $from = $dsn->getRequiredOption('from'); + + $host = 'default' === $dsn->getHost() ? null : $dsn->getHost(); + $port = $dsn->getPort(); + + return (new LightSmsTransport($login, $token, $from, $this->client, $this->dispatcher))->setHost($host)->setPort($port); + } + + protected function getSupportedSchemes(): array + { + return ['lightsms']; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/LightSms/README.md b/src/Symfony/Component/Notifier/Bridge/LightSms/README.md new file mode 100644 index 0000000000000..0414e5e1018b9 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/LightSms/README.md @@ -0,0 +1,26 @@ +LightSms Notifier +================= + +Provides [LightSms](https://www.lightsms.com/) integration for Symfony Notifier. + +DSN example +----------- + +``` +LIGHTSMS_DSN=lightsms://LOGIN:TOKEN@default?from=PHONE +``` + +where: + - `LOGIN` is your LightSms login + - `TOKEN` is the token displayed in your account + - `PHONE` is your LightSms sender phone number + +See your account info at https://www.lightsms.com/external/client/api/ + +Resources +--------- + + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/src/Symfony/Component/Notifier/Bridge/LightSms/Tests/LightSmsTransportFactoryTest.php b/src/Symfony/Component/Notifier/Bridge/LightSms/Tests/LightSmsTransportFactoryTest.php new file mode 100644 index 0000000000000..01b206882f549 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/LightSms/Tests/LightSmsTransportFactoryTest.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\LightSms\Tests; + +use Symfony\Component\Notifier\Bridge\LightSms\LightSmsTransportFactory; +use Symfony\Component\Notifier\Test\TransportFactoryTestCase; +use Symfony\Component\Notifier\Transport\TransportFactoryInterface; + +final class LightSmsTransportFactoryTest extends TransportFactoryTestCase +{ + /** + * @return LightSmsTransportFactory + */ + public function createFactory(): TransportFactoryInterface + { + return new LightSmsTransportFactory(); + } + + public function createProvider(): iterable + { + yield [ + 'lightsms://host.test?from=0611223344', + 'lightsms://login:token@host.test?from=0611223344', + ]; + } + + public function supportsProvider(): iterable + { + yield [true, 'lightsms://login:token@default?from=37061234567']; + yield [false, 'somethingElse://login:token@default?from=37061234567']; + } + + public function unsupportedSchemeProvider(): iterable + { + yield ['somethingElse://login:token@default?from=37061234567']; + yield ['somethingElse://login:token@default']; // missing "from" option + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/LightSms/Tests/LightSmsTransportTest.php b/src/Symfony/Component/Notifier/Bridge/LightSms/Tests/LightSmsTransportTest.php new file mode 100644 index 0000000000000..86230a9eb8789 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/LightSms/Tests/LightSmsTransportTest.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\LightSms\Tests; + +use Symfony\Component\Notifier\Bridge\LightSms\LightSmsTransport; +use Symfony\Component\Notifier\Message\ChatMessage; +use Symfony\Component\Notifier\Message\MessageInterface; +use Symfony\Component\Notifier\Message\SmsMessage; +use Symfony\Component\Notifier\Test\TransportTestCase; +use Symfony\Component\Notifier\Transport\TransportInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +final class LightSmsTransportTest extends TransportTestCase +{ + /** + * @return LightSmsTransport + */ + public function createTransport(?HttpClientInterface $client = null): TransportInterface + { + return new LightSmsTransport('accountSid', 'authToken', 'from', $client ?: $this->createMock(HttpClientInterface::class)); + } + + public function toStringProvider(): iterable + { + yield ['lightsms://www.lightsms.com?from=from', $this->createTransport()]; + } + + public function supportedMessagesProvider(): iterable + { + yield [new SmsMessage('0611223344', 'Hello!')]; + } + + public function unsupportedMessagesProvider(): iterable + { + yield [new ChatMessage('Hello!')]; + yield [$this->createMock(MessageInterface::class)]; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/LightSms/composer.json b/src/Symfony/Component/Notifier/Bridge/LightSms/composer.json new file mode 100644 index 0000000000000..5faaa6f0ffc07 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/LightSms/composer.json @@ -0,0 +1,30 @@ +{ + "name": "symfony/lightsms-notifier", + "type": "symfony-bridge", + "description": "Symfony LightSms Notifier Bridge", + "keywords": ["sms", "light-sms", "notifier"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Vasilij Duško", + "email": "vasilij@prado.lt" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.2.5", + "symfony/http-client": "^4.4|^5.2", + "symfony/notifier": "^5.3" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\Notifier\\Bridge\\LightSms\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" +} diff --git a/src/Symfony/Component/Notifier/Bridge/LightSms/phpunit.xml.dist b/src/Symfony/Component/Notifier/Bridge/LightSms/phpunit.xml.dist new file mode 100644 index 0000000000000..6eddaf643af25 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/LightSms/phpunit.xml.dist @@ -0,0 +1,31 @@ + + + + + + + + + + ./Tests/ + + + + + + ./ + + ./Resources + ./Tests + ./vendor + + + + diff --git a/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php b/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php index 06bb494d5f86c..3a28762bdda49 100644 --- a/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php +++ b/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php @@ -116,6 +116,10 @@ class UnsupportedSchemeException extends LogicException 'class' => Bridge\Clickatell\ClickatellTransportFactory::class, 'package' => 'symfony/clickatell-notifier', ], + 'lightsms' => [ + 'class' => Bridge\LightSms\LightSmsTransportFactory::class, + 'package' => 'symfony/lightsms-notifier', + ], ]; /** diff --git a/src/Symfony/Component/Notifier/Transport.php b/src/Symfony/Component/Notifier/Transport.php index b2430bbe9ac2d..76feae13a1b41 100644 --- a/src/Symfony/Component/Notifier/Transport.php +++ b/src/Symfony/Component/Notifier/Transport.php @@ -21,6 +21,7 @@ use Symfony\Component\Notifier\Bridge\Gitter\GitterTransportFactory; use Symfony\Component\Notifier\Bridge\Infobip\InfobipTransportFactory; use Symfony\Component\Notifier\Bridge\Iqsms\IqsmsTransportFactory; +use Symfony\Component\Notifier\Bridge\LightSms\LightSmsTransportFactory; use Symfony\Component\Notifier\Bridge\Mattermost\MattermostTransportFactory; use Symfony\Component\Notifier\Bridge\Mobyt\MobytTransportFactory; use Symfony\Component\Notifier\Bridge\Nexmo\NexmoTransportFactory; @@ -74,6 +75,7 @@ class Transport OctopushTransportFactory::class, GitterTransportFactory::class, ClickatellTransportFactory::class, + LightSmsTransportFactory::class, ]; private $factories;