diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index b0d7182aa4b1..f7e10d2937dc 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -122,6 +122,7 @@ use Symfony\Component\Notifier\Bridge\FakeChat\FakeChatTransportFactory; use Symfony\Component\Notifier\Bridge\FakeSms\FakeSmsTransportFactory; use Symfony\Component\Notifier\Bridge\Firebase\FirebaseTransportFactory; +use Symfony\Component\Notifier\Bridge\FortySixElks\FortySixElksTransportFactory; use Symfony\Component\Notifier\Bridge\FreeMobile\FreeMobileTransportFactory; use Symfony\Component\Notifier\Bridge\GatewayApi\GatewayApiTransportFactory; use Symfony\Component\Notifier\Bridge\Gitter\GitterTransportFactory; @@ -2421,6 +2422,7 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $ FakeChatTransportFactory::class => 'notifier.transport_factory.fake-chat', FakeSmsTransportFactory::class => 'notifier.transport_factory.fake-sms', FirebaseTransportFactory::class => 'notifier.transport_factory.firebase', + FortySixElksTransportFactory::class => 'notifier.transport_factory.forty-six-elks', FreeMobileTransportFactory::class => 'notifier.transport_factory.free-mobile', GatewayApiTransportFactory::class => 'notifier.transport_factory.gateway-api', GitterTransportFactory::class => 'notifier.transport_factory.gitter', diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php index 0fe235f88f4b..4a4bf34bdc8e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php @@ -20,6 +20,7 @@ use Symfony\Component\Notifier\Bridge\FakeChat\FakeChatTransportFactory; use Symfony\Component\Notifier\Bridge\FakeSms\FakeSmsTransportFactory; use Symfony\Component\Notifier\Bridge\Firebase\FirebaseTransportFactory; +use Symfony\Component\Notifier\Bridge\FortySixElks\FortySixElksTransportFactory; use Symfony\Component\Notifier\Bridge\FreeMobile\FreeMobileTransportFactory; use Symfony\Component\Notifier\Bridge\GatewayApi\GatewayApiTransportFactory; use Symfony\Component\Notifier\Bridge\Gitter\GitterTransportFactory; @@ -105,6 +106,10 @@ ->parent('notifier.transport_factory.abstract') ->tag('chatter.transport_factory') + ->set('notifier.transport_factory.forty-six-elks', FortySixElksTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + ->set('notifier.transport_factory.free-mobile', FreeMobileTransportFactory::class) ->parent('notifier.transport_factory.abstract') ->tag('texter.transport_factory') diff --git a/src/Symfony/Component/Notifier/Bridge/FortySixElks/.gitattributes b/src/Symfony/Component/Notifier/Bridge/FortySixElks/.gitattributes new file mode 100644 index 000000000000..84c7add058fb --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/FortySixElks/.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/FortySixElks/.gitignore b/src/Symfony/Component/Notifier/Bridge/FortySixElks/.gitignore new file mode 100644 index 000000000000..c49a5d8df5c6 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/FortySixElks/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/src/Symfony/Component/Notifier/Bridge/FortySixElks/CHANGELOG.md b/src/Symfony/Component/Notifier/Bridge/FortySixElks/CHANGELOG.md new file mode 100644 index 000000000000..72f7d401b661 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/FortySixElks/CHANGELOG.md @@ -0,0 +1,7 @@ +CHANGELOG +========= + +6.1 +--- + + * Add the bridge diff --git a/src/Symfony/Component/Notifier/Bridge/FortySixElks/FortySixElksTransport.php b/src/Symfony/Component/Notifier/Bridge/FortySixElks/FortySixElksTransport.php new file mode 100644 index 000000000000..706a0f162856 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/FortySixElks/FortySixElksTransport.php @@ -0,0 +1,87 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\FortySixElks; + +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\Exception\TransportExceptionInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +/** + * @author Jon Gotlin + */ +final class FortySixElksTransport extends AbstractTransport +{ + protected const HOST = 'api.46elks.com'; + + private string $apiUsername; + private string $apiPassword; + private string $from; + + public function __construct(string $apiUsername, string $apiPassword, string $from, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null) + { + $this->apiUsername = $apiUsername; + $this->apiPassword = $apiPassword; + $this->from = $from; + + parent::__construct($client, $dispatcher); + } + + public function __toString(): string + { + return sprintf('forty-six-elks://%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); + } + + $endpoint = sprintf('https://%s/a1/sms', self::HOST); + $response = $this->client->request('POST', $endpoint, [ + 'body' => [ + 'from' => $this->from, + 'to' => $message->getPhone(), + 'message' => $message->getSubject(), + ], + 'auth_basic' => [$this->apiUsername, $this->apiPassword], + ]); + + try { + $statusCode = $response->getStatusCode(); + } catch (TransportExceptionInterface $e) { + throw new TransportException('Could not reach the remote 46elks server.', $response, 0, $e); + } + + if (200 !== $statusCode) { + throw new TransportException('Unable to post the 46elks message: '.$response->getContent(false), $response); + } + + $result = $response->toArray(false); + + $sentMessage = new SentMessage($message, (string) $this); + $sentMessage->setMessageId($result['id'] ?? ''); + + return $sentMessage; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/FortySixElks/FortySixElksTransportFactory.php b/src/Symfony/Component/Notifier/Bridge/FortySixElks/FortySixElksTransportFactory.php new file mode 100644 index 000000000000..5622e06f7757 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/FortySixElks/FortySixElksTransportFactory.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\FortySixElks; + +use Symfony\Component\Notifier\Exception\UnsupportedSchemeException; +use Symfony\Component\Notifier\Transport\AbstractTransportFactory; +use Symfony\Component\Notifier\Transport\Dsn; + +/** + * @author Jon Gotlin + */ +final class FortySixElksTransportFactory extends AbstractTransportFactory +{ + public function create(Dsn $dsn): FortySixElksTransport + { + if ('forty-six-elks' !== $dsn->getScheme()) { + throw new UnsupportedSchemeException($dsn, 'forty-six-elks', $this->getSupportedSchemes()); + } + + $host = 'default' === $dsn->getHost() ? null : $dsn->getHost(); + $from = $dsn->getRequiredOption('from'); + + return (new FortySixElksTransport($this->getUser($dsn), $this->getPassword($dsn), $from, $this->client, $this->dispatcher))->setHost($host)->setPort($dsn->getPort()); + } + + protected function getSupportedSchemes(): array + { + return ['forty-six-elks']; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/FortySixElks/LICENSE b/src/Symfony/Component/Notifier/Bridge/FortySixElks/LICENSE new file mode 100644 index 000000000000..0ece8964f767 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/FortySixElks/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2022 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/FortySixElks/README.md b/src/Symfony/Component/Notifier/Bridge/FortySixElks/README.md new file mode 100644 index 000000000000..2be99f09e68a --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/FortySixElks/README.md @@ -0,0 +1,24 @@ +46elks Notifier +=============== + +Provides [46elks](https://46elks.se) integration for Symfony Notifier. + +DSN example +----------- + +``` +FORTY_SIX_ELKS_DSN=forty-six-elks://API_USERNAME:API_PASSWORD@default?from=FROM +``` + +where: + - `API_USERNAME` is your 46elks API username + - `API_PASSWORD` is your 46elks API password + - `FROM` is the alphanumeric originator for the message to appear to originate from + +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/FortySixElks/Tests/Fixtures/success-response.json b/src/Symfony/Component/Notifier/Bridge/FortySixElks/Tests/Fixtures/success-response.json new file mode 100644 index 000000000000..0f68050e4700 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/FortySixElks/Tests/Fixtures/success-response.json @@ -0,0 +1,11 @@ +{ + "status": "created", + "direction": "outgoing", + "from": "+46702222222", + "created": "2021-12-30T20:35:32.429389", + "parts": 1, + "to": "+46701111111", + "cost": 3900, + "message": "Symfony test", + "id": "s0231d6d7d6bc14a7e7734e466785c4ce" +} diff --git a/src/Symfony/Component/Notifier/Bridge/FortySixElks/Tests/FortySixElksTransportFactoryTest.php b/src/Symfony/Component/Notifier/Bridge/FortySixElks/Tests/FortySixElksTransportFactoryTest.php new file mode 100644 index 000000000000..7237060d33bf --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/FortySixElks/Tests/FortySixElksTransportFactoryTest.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + */ + +namespace Symfony\Component\Notifier\Bridge\FortySixElks\Tests; + +use Symfony\Component\Notifier\Bridge\FortySixElks\FortySixElksTransportFactory; +use Symfony\Component\Notifier\Test\TransportFactoryTestCase; + +class FortySixElksTransportFactoryTest extends TransportFactoryTestCase +{ + public function createFactory(): FortySixElksTransportFactory + { + return new FortySixElksTransportFactory(); + } + + public function createProvider(): iterable + { + yield [ + 'forty-six-elks://host.test?from=Symfony', + 'forty-six-elks://api_username:api_password@host.test?from=Symfony', + ]; + } + + public function supportsProvider(): iterable + { + yield [true, 'forty-six-elks://api_key@default']; + yield [false, 'somethingElse://api_key@default']; + } + + public function unsupportedSchemeProvider(): iterable + { + yield ['somethingElse://api_key@default']; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/FortySixElks/Tests/FortySixElksTransportTest.php b/src/Symfony/Component/Notifier/Bridge/FortySixElks/Tests/FortySixElksTransportTest.php new file mode 100644 index 000000000000..c23268d62944 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/FortySixElks/Tests/FortySixElksTransportTest.php @@ -0,0 +1,91 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\FortySixElks\Tests; + +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\Notifier\Bridge\FortySixElks\FortySixElksTransport; +use Symfony\Component\Notifier\Exception\TransportException; +use Symfony\Component\Notifier\Message\ChatMessage; +use Symfony\Component\Notifier\Message\MessageInterface; +use Symfony\Component\Notifier\Message\SentMessage; +use Symfony\Component\Notifier\Message\SmsMessage; +use Symfony\Component\Notifier\Test\TransportTestCase; +use Symfony\Contracts\HttpClient\HttpClientInterface; +use Symfony\Contracts\HttpClient\ResponseInterface; + +class FortySixElksTransportTest extends TransportTestCase +{ + public function createTransport(HttpClientInterface $client = null): FortySixElksTransport + { + return new FortySixElksTransport('api_username', 'api_password', 'Symfony', $client ?? $this->createMock(HttpClientInterface::class)); + } + + public function toStringProvider(): iterable + { + yield ['forty-six-elks://api.46elks.com?from=Symfony', $this->createTransport()]; + } + + public function supportedMessagesProvider(): iterable + { + yield [new SmsMessage('+46701111111', 'Hello!')]; + } + + public function unsupportedMessagesProvider(): iterable + { + yield [new ChatMessage('Hello!')]; + yield [$this->createMock(MessageInterface::class)]; + } + + public function testSendSuccessfully() + { + $response = $this->createMock(ResponseInterface::class); + $response->method('getStatusCode')->willReturn(200); + $response->method('getContent')->willReturn(file_get_contents(__DIR__.'/Fixtures/success-response.json')); + $client = new MockHttpClient($response); + $transport = $this->createTransport($client); + $sentMessage = $transport->send(new SmsMessage('+46701111111', 'Hello!')); + + $this->assertInstanceOf(SentMessage::class, $sentMessage); + $this->assertSame('s0231d6d7d6bc14a7e7734e466785c4ce', $sentMessage->getMessageId()); + } + + /** + * @dataProvider errorProvider + */ + public function testExceptionIsThrownWhenSendFailed(int $statusCode, string $content, string $expectedExceptionMessage) + { + $response = $this->createMock(ResponseInterface::class); + $response->method('getStatusCode')->willReturn($statusCode); + $response->method('getContent')->willReturn($content); + $client = new MockHttpClient($response); + $transport = $this->createTransport($client); + + $this->expectException(TransportException::class); + $this->expectExceptionMessage($expectedExceptionMessage); + + $transport->send(new SmsMessage('+46701111111', 'Hello!')); + } + + public function errorProvider(): iterable + { + yield [ + 401, + 'API access requires Basic HTTP authentication. Read documentation or examples.', + 'Unable to post the 46elks message: API access requires Basic HTTP authentication. Read documentation or examples.', + ]; + yield [ + 403, + 'Missing key from', + 'Unable to post the 46elks message: Missing key from', + ]; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/FortySixElks/composer.json b/src/Symfony/Component/Notifier/Bridge/FortySixElks/composer.json new file mode 100644 index 000000000000..fb64293a68c2 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/FortySixElks/composer.json @@ -0,0 +1,30 @@ +{ + "name": "symfony/forty-six-elks-notifier", + "type": "symfony-notifier-bridge", + "description": "Symfony 46elks Notifier Bridge", + "keywords": ["sms", "46elks", "forty-six-elks", "notifier"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Jon Gotlin", + "email": "jon@jon.se" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=8.0.2", + "symfony/http-client": "^5.4|^6.0", + "symfony/notifier": "^5.4|^6.0" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\Notifier\\Bridge\\FortySixElks\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" +} diff --git a/src/Symfony/Component/Notifier/Bridge/FortySixElks/phpunit.xml.dist b/src/Symfony/Component/Notifier/Bridge/FortySixElks/phpunit.xml.dist new file mode 100644 index 000000000000..865ca790199b --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/FortySixElks/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 5245c8463734..27223e1de4b6 100644 --- a/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php +++ b/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php @@ -52,6 +52,10 @@ class UnsupportedSchemeException extends LogicException 'class' => Bridge\Firebase\FirebaseTransportFactory::class, 'package' => 'symfony/firebase-notifier', ], + 'forty-six-elks' => [ + 'class' => Bridge\FortySixElks\FortySixElksTransportFactory::class, + 'package' => 'symfony/forty-six-elks-notifier', + ], 'freemobile' => [ 'class' => Bridge\FreeMobile\FreeMobileTransportFactory::class, 'package' => 'symfony/free-mobile-notifier', diff --git a/src/Symfony/Component/Notifier/Tests/Exception/UnsupportedSchemeExceptionTest.php b/src/Symfony/Component/Notifier/Tests/Exception/UnsupportedSchemeExceptionTest.php index 66c04cc68d73..2deddb90bb7a 100644 --- a/src/Symfony/Component/Notifier/Tests/Exception/UnsupportedSchemeExceptionTest.php +++ b/src/Symfony/Component/Notifier/Tests/Exception/UnsupportedSchemeExceptionTest.php @@ -21,6 +21,7 @@ use Symfony\Component\Notifier\Bridge\FakeChat\FakeChatTransportFactory; use Symfony\Component\Notifier\Bridge\FakeSms\FakeSmsTransportFactory; use Symfony\Component\Notifier\Bridge\Firebase\FirebaseTransportFactory; +use Symfony\Component\Notifier\Bridge\FortySixElks\FortySixElksTransportFactory; use Symfony\Component\Notifier\Bridge\FreeMobile\FreeMobileTransportFactory; use Symfony\Component\Notifier\Bridge\GatewayApi\GatewayApiTransportFactory; use Symfony\Component\Notifier\Bridge\Gitter\GitterTransportFactory; @@ -75,6 +76,7 @@ public static function setUpBeforeClass(): void FakeChatTransportFactory::class => false, FakeSmsTransportFactory::class => false, FirebaseTransportFactory::class => false, + FortySixElksTransportFactory::class => false, FreeMobileTransportFactory::class => false, GatewayApiTransportFactory::class => false, GitterTransportFactory::class => false, diff --git a/src/Symfony/Component/Notifier/Transport.php b/src/Symfony/Component/Notifier/Transport.php index ca61a255466c..6cc3d3f137ca 100644 --- a/src/Symfony/Component/Notifier/Transport.php +++ b/src/Symfony/Component/Notifier/Transport.php @@ -18,6 +18,7 @@ use Symfony\Component\Notifier\Bridge\Esendex\EsendexTransportFactory; use Symfony\Component\Notifier\Bridge\Expo\ExpoTransportFactory; use Symfony\Component\Notifier\Bridge\Firebase\FirebaseTransportFactory; +use Symfony\Component\Notifier\Bridge\FortySixElks\FortySixElksTransportFactory; use Symfony\Component\Notifier\Bridge\FreeMobile\FreeMobileTransportFactory; use Symfony\Component\Notifier\Bridge\GatewayApi\GatewayApiTransportFactory; use Symfony\Component\Notifier\Bridge\Gitter\GitterTransportFactory; @@ -72,6 +73,7 @@ final class Transport EsendexTransportFactory::class, ExpoTransportFactory::class, FirebaseTransportFactory::class, + FortySixElksTransportFactory::class, FreeMobileTransportFactory::class, GatewayApiTransportFactory::class, GitterTransportFactory::class,