From 3e5bf699571e4bafc3f991b2dd59f0f769cf6ad2 Mon Sep 17 00:00:00 2001 From: Cesur APAYDIN Date: Mon, 1 Jan 2024 05:44:52 +0300 Subject: [PATCH 1/9] [FirebaseNotifier] Add 'HTTP v1' api endpoint --- .../Bridge/Firebase/FirebaseJwtTransport.php | 124 ++++++++++++++++++ .../Firebase/FirebaseTransportFactory.php | 17 ++- .../Notifier/Bridge/Firebase/README.md | 15 ++- .../Tests/FirebaseJwtTransportFactoryTest.php | 46 +++++++ .../Tests/FirebaseJwtTransportTest.php | 84 ++++++++++++ .../Notifier/Bridge/Firebase/composer.json | 3 +- 6 files changed, 283 insertions(+), 6 deletions(-) create mode 100644 src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseJwtTransport.php create mode 100644 src/Symfony/Component/Notifier/Bridge/Firebase/Tests/FirebaseJwtTransportFactoryTest.php create mode 100644 src/Symfony/Component/Notifier/Bridge/Firebase/Tests/FirebaseJwtTransportTest.php diff --git a/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseJwtTransport.php b/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseJwtTransport.php new file mode 100644 index 0000000000000..92dc1d55dd54c --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseJwtTransport.php @@ -0,0 +1,124 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Firebase; + +use Ahc\Jwt\JWT; +use Symfony\Component\Notifier\Exception\InvalidArgumentException; +use Symfony\Component\Notifier\Exception\TransportException; +use Symfony\Component\Notifier\Exception\UnsupportedMessageTypeException; +use Symfony\Component\Notifier\Message\ChatMessage; +use Symfony\Component\Notifier\Message\MessageInterface; +use Symfony\Component\Notifier\Message\SentMessage; +use Symfony\Component\Notifier\Transport\AbstractTransport; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; +use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +/** + * @author Cesur APAYDIN + */ +final class FirebaseJwtTransport extends AbstractTransport +{ + protected const HOST = "fcm.googleapis.com/v1/projects/project_id/messages:send"; + + private array $credentials; + + public function __construct(#[\SensitiveParameter] array $credentials, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null) + { + $this->credentials = $credentials; + $this->client = $client; + + $this->setHost(str_replace('project_id', $credentials['project_id'], $this->getDefaultHost())); + + parent::__construct($client, $dispatcher); + } + + public function __toString(): string + { + return sprintf('firebase-jwt://%s', $this->getEndpoint()); + } + + public function supports(MessageInterface $message): bool + { + return $message instanceof ChatMessage && (null === $message->getOptions() || $message->getOptions() instanceof FirebaseOptions); + } + + protected function doSend(MessageInterface $message): SentMessage + { + if (!$message instanceof ChatMessage) { + throw new UnsupportedMessageTypeException(__CLASS__, ChatMessage::class, $message); + } + + $endpoint = sprintf('https://%s', $this->getEndpoint()); + $options = $message->getOptions()?->toArray() ?? []; + $options['token'] = $message->getRecipientId(); + unset($options['to']); + + if (!$options['token']) { + throw new InvalidArgumentException(sprintf('The "%s" transport required the "to" option to be set.', __CLASS__)); + } + $options['notification']['body'] = $message->getSubject(); + $options['data'] ??= []; + + // Send + $response = $this->client->request('POST', $endpoint, [ + 'headers' => [ + 'Authorization' => sprintf('Bearer %s', $this->getJwtToken()), + ], + 'json' => array_filter(['message' => $options]), + ]); + + try { + $statusCode = $response->getStatusCode(); + } catch (TransportExceptionInterface $e) { + throw new TransportException('Could not reach the remote Firebase server.', $response, 0, $e); + } + + $contentType = $response->getHeaders(false)['content-type'][0] ?? ''; + $jsonContents = str_starts_with($contentType, 'application/json') ? $response->toArray(false) : null; + $errorMessage = null; + + if ($jsonContents && isset($jsonContents['results'][0]['error'])) { + $errorMessage = $jsonContents['results'][0]['error']; + } elseif (200 !== $statusCode) { + $errorMessage = $response->getContent(false); + } + + if (null !== $errorMessage) { + throw new TransportException('Unable to post the Firebase message: ' . $errorMessage, $response); + } + + $success = $response->toArray(false); + + $sentMessage = new SentMessage($message, (string)$this); + $sentMessage->setMessageId($success['results'][0]['message_id'] ?? ''); + + return $sentMessage; + } + + private function getJwtToken(): string + { + $time = time(); + $payload = [ + 'iss' => $this->credentials['client_email'], + 'sub' => $this->credentials['client_email'], + 'aud' => 'https://fcm.googleapis.com/', + 'iat' => $time, + 'exp' => $time + 3600, + 'kid' => $this->credentials['private_key_id'] + ]; + + $jwt = new JWT(openssl_pkey_get_private($this->credentials['private_key']), 'RS256'); + + return $jwt->encode($payload); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseTransportFactory.php b/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseTransportFactory.php index 808d7470ce6d5..5bd2845500b2f 100644 --- a/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseTransportFactory.php +++ b/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseTransportFactory.php @@ -20,9 +20,12 @@ */ final class FirebaseTransportFactory extends AbstractTransportFactory { - public function create(Dsn $dsn): FirebaseTransport + public function create(Dsn $dsn): FirebaseTransport|FirebaseJwtTransport { $scheme = $dsn->getScheme(); + if ('firebase-jwt' === $scheme) { + return $this->createJwt($dsn); + } if ('firebase' !== $scheme) { throw new UnsupportedSchemeException($dsn, 'firebase', $this->getSupportedSchemes()); @@ -35,8 +38,18 @@ public function create(Dsn $dsn): FirebaseTransport return (new FirebaseTransport($token, $this->client, $this->dispatcher))->setHost($host)->setPort($port); } + public function createJwt(Dsn $dsn): FirebaseJwtTransport + { + $credentials = match ($this->getUser($dsn)) { + 'credentials_path' => file_get_contents($this->getPassword($dsn)), + 'credentials_content' => base64_decode($this->getPassword($dsn)), + }; + + return (new FirebaseJwtTransport(json_decode($credentials, true, 512, JSON_THROW_ON_ERROR), $this->client, $this->dispatcher)); + } + protected function getSupportedSchemes(): array { - return ['firebase']; + return ['firebase', 'firebase-jwt']; } } diff --git a/src/Symfony/Component/Notifier/Bridge/Firebase/README.md b/src/Symfony/Component/Notifier/Bridge/Firebase/README.md index 7ff2c71575c88..85faf04ece19d 100644 --- a/src/Symfony/Component/Notifier/Bridge/Firebase/README.md +++ b/src/Symfony/Component/Notifier/Bridge/Firebase/README.md @@ -3,7 +3,7 @@ Firebase Notifier Provides [Firebase](https://firebase.google.com) integration for Symfony Notifier. -DSN example +Legacy DSN example ----------- ``` @@ -11,8 +11,17 @@ FIREBASE_DSN=firebase://USERNAME:PASSWORD@default ``` where: - - `USERNAME` is your Firebase username - - `PASSWORD` is your Firebase password +- `USERNAME` is your Firebase username +- `PASSWORD` is your Firebase password + +JWT DSN example (HTTP v1) +----------- + +``` +FIREBASE_DSN=firebase-jwt://credentials_path:@default +FIREBASE_DSN=firebase-jwt://credentials_content:@default +``` + Adding Interactions to a Message -------------------------------- diff --git a/src/Symfony/Component/Notifier/Bridge/Firebase/Tests/FirebaseJwtTransportFactoryTest.php b/src/Symfony/Component/Notifier/Bridge/Firebase/Tests/FirebaseJwtTransportFactoryTest.php new file mode 100644 index 0000000000000..9000480830990 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Firebase/Tests/FirebaseJwtTransportFactoryTest.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Firebase\Tests; + +use Symfony\Component\Notifier\Bridge\Firebase\FirebaseTransportFactory; +use Symfony\Component\Notifier\Test\TransportFactoryTestCase; + +/** + * @author Cesur APAYDIN + */ +final class FirebaseJwtTransportFactoryTest extends TransportFactoryTestCase +{ + public function createFactory(): FirebaseTransportFactory + { + return new FirebaseTransportFactory(); + } + + public static function createProvider(): iterable + { + yield [ + 'firebase-jwt://fcm.googleapis.com/v1/projects/test_project/messages:send', + 'firebase-jwt://credentials_content:ewogICJ0eXBlIjogIiIsCiAgInByb2plY3RfaWQiOiAidGVzdF9wcm9qZWN0Igp9Cg==@default', + ]; + } + + public static function supportsProvider(): iterable + { + yield [true, 'firebase-jwt://credentials_path:crendentials.json@default']; + yield [true, 'firebase-jwt://credentials_content:base64Content@default']; + yield [false, 'somethingElse://username:password@default']; + } + + public static function unsupportedSchemeProvider(): iterable + { + yield ['somethingElse://username:password@default']; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Firebase/Tests/FirebaseJwtTransportTest.php b/src/Symfony/Component/Notifier/Bridge/Firebase/Tests/FirebaseJwtTransportTest.php new file mode 100644 index 0000000000000..a1c6c87c30fa4 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Firebase/Tests/FirebaseJwtTransportTest.php @@ -0,0 +1,84 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Firebase\Tests; + +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\HttpClient\Response\MockResponse; +use Symfony\Component\Notifier\Bridge\Firebase\FirebaseJwtTransport; +use Symfony\Component\Notifier\Bridge\Firebase\FirebaseOptions; +use Symfony\Component\Notifier\Exception\TransportException; +use Symfony\Component\Notifier\Message\ChatMessage; +use Symfony\Component\Notifier\Message\SmsMessage; +use Symfony\Component\Notifier\Test\TransportTestCase; +use Symfony\Component\Notifier\Tests\Transport\DummyMessage; +use Symfony\Contracts\HttpClient\HttpClientInterface; +use Symfony\Contracts\HttpClient\ResponseInterface; + +/** + * @author Cesur APAYDIN + */ +final class FirebaseJwtTransportTest extends TransportTestCase +{ + public static function createTransport(HttpClientInterface $client = null): FirebaseJwtTransport + { + return new FirebaseJwtTransport([ + 'project_id' => 'test_project', + 'client_email' => 'firebase-adminsdk-test@test.iam.gserviceaccount.com', + 'private_key_id' => 'sdas7d6a8ds6ds78a', + 'private_key' => "-----BEGIN RSA PRIVATE KEY-----\nMIICWwIBAAKBgGN4fgq4BFQwjK7kzWUYSFE1ryGIBtUScY5TqLY2BAROBnZS+SIa\nH4VcZJStPUwjtsVxJTf57slhMM5FbAOQkWFMmRlHGWc7EZy6UMMvP8FD21X3Ty9e\nZzJ/Be30la1Uy7rechBh3RN+Y3rSKV+gDmsjdo5/4Jekj4LfluDXbwVJAgMBAAEC\ngYA5SqY2IEUGBKyS81/F8ZV9iNElHAbrZGMZWeAbisMHg7U/I40w8iDjnBKme52J\npCxaTk/kjMTXIm6M7/lFmFfTHgl5WLCimu2glMyKFM2GBYX/cKx9RnI36q3uJYml\n1G1f2H7ALurisenEqMaq8bdyApd/XNqcijogfsZ1K/irTQJBAKEQFkqNDgwUgAwr\njhG/zppl5yEJtP+Pncp/2t/s6khk0q8N92xw6xl8OV/ww+rwlJB3IKVKw903LztQ\nP1D3zpMCQQCeGlOvMx9XxiktNIkdXekGP/bFUR9/u0ABaYl9valZ2B3yZzujJJHV\n0EtyKGorT39wWhWY7BI8NTYgivCIWGozAkEAhMnOlwhUXIFKUL5YEyogHAuH0yU9\npLWzUhC3U4bwYV8+lDTfmPg/3HMemorV/Az9b13H/H73nJqyxiQTD54/IQJAZUX/\n7O4WWac5oRdR7VnGdpZqgCJixvMvILh1tfHTlRV2uVufO/Wk5Q00BsAUogGeZF2Q\nEBDH7YE4VsgpI21fOQJAJdSB7mHvStlYCQMEAYWCWjk+NRW8fzZCkQkqzOV6b9dw\nDFp6wp8aLw87hAHUz5zXTCRYi/BpvDhfP6DDT2sOaw==\n-----END RSA PRIVATE KEY-----" + ], $client ?? new MockHttpClient()); + } + + public static function toStringProvider(): iterable + { + yield ['firebase-jwt://fcm.googleapis.com/v1/projects/test_project/messages:send', self::createTransport()]; + } + + public static function supportedMessagesProvider(): iterable + { + yield [new ChatMessage('Hello!')]; + } + + public static function unsupportedMessagesProvider(): iterable + { + yield [new SmsMessage('0611223344', 'Hello!')]; + yield [new DummyMessage()]; + } + + /** + * @dataProvider sendWithErrorThrowsExceptionProvider + */ + public function testSendWithErrorThrowsTransportException(ResponseInterface $response) + { + $this->expectException(TransportException::class); + + $client = new MockHttpClient(static fn (): ResponseInterface => $response); + $options = new class('recipient-id', []) extends FirebaseOptions {}; + + $transport = self::createTransport($client); + + $transport->send(new ChatMessage('Hello!', $options)); + } + + public static function sendWithErrorThrowsExceptionProvider(): iterable + { + yield [new MockResponse( + json_encode(['results' => [['error' => 'testErrorCode']]]), + ['response_headers' => ['content-type' => ['application/json']], 'http_code' => 200] + )]; + + yield [new MockResponse( + json_encode(['results' => [['error' => 'testErrorCode']]]), + ['response_headers' => ['content-type' => ['application/json']], 'http_code' => 400] + )]; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Firebase/composer.json b/src/Symfony/Component/Notifier/Bridge/Firebase/composer.json index 466474bc02e52..5dd4cff038268 100644 --- a/src/Symfony/Component/Notifier/Bridge/Firebase/composer.json +++ b/src/Symfony/Component/Notifier/Bridge/Firebase/composer.json @@ -18,7 +18,8 @@ "require": { "php": ">=8.2", "symfony/http-client": "^6.4|^7.0", - "symfony/notifier": "^6.4|^7.0" + "symfony/notifier": "^6.4|^7.0", + "adhocore/jwt": "^1.1" }, "autoload": { "psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Firebase\\": "" }, From ac9002ff46d8d16ee06d8d7fe2225d6c019e8ae8 Mon Sep 17 00:00:00 2001 From: Cesur APAYDIN Date: Mon, 1 Jan 2024 07:09:38 +0300 Subject: [PATCH 2/9] Removed Jwt Package Dependency --- .../Bridge/Firebase/FirebaseJwtTransport.php | 19 +++++++++++++++---- .../Notifier/Bridge/Firebase/composer.json | 4 ++-- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseJwtTransport.php b/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseJwtTransport.php index 92dc1d55dd54c..f9d4a3fb8b4f8 100644 --- a/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseJwtTransport.php +++ b/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseJwtTransport.php @@ -11,7 +11,6 @@ namespace Symfony\Component\Notifier\Bridge\Firebase; -use Ahc\Jwt\JWT; use Symfony\Component\Notifier\Exception\InvalidArgumentException; use Symfony\Component\Notifier\Exception\TransportException; use Symfony\Component\Notifier\Exception\UnsupportedMessageTypeException; @@ -114,11 +113,23 @@ private function getJwtToken(): string 'aud' => 'https://fcm.googleapis.com/', 'iat' => $time, 'exp' => $time + 3600, - 'kid' => $this->credentials['private_key_id'] + 'kid' => $this->credentials['private_key_id'], ]; - $jwt = new JWT(openssl_pkey_get_private($this->credentials['private_key']), 'RS256'); + $header = $this->urlSafeEncode(['alg' => 'RS256', 'typ' => 'JWT']); + $payload = $this->urlSafeEncode($payload); + openssl_sign($header . '.' . $payload, $signature, openssl_pkey_get_private($this->credentials['private_key']), OPENSSL_ALGO_SHA256); + $signature = $this->urlSafeEncode($signature); - return $jwt->encode($payload); + return $header . '.' . $payload . '.' . $signature; + } + + protected function urlSafeEncode($data): string + { + if (is_array($data)) { + $data = json_encode($data, JSON_UNESCAPED_SLASHES); + } + + return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); } } diff --git a/src/Symfony/Component/Notifier/Bridge/Firebase/composer.json b/src/Symfony/Component/Notifier/Bridge/Firebase/composer.json index 5dd4cff038268..7d9fc4debd3e8 100644 --- a/src/Symfony/Component/Notifier/Bridge/Firebase/composer.json +++ b/src/Symfony/Component/Notifier/Bridge/Firebase/composer.json @@ -17,9 +17,9 @@ ], "require": { "php": ">=8.2", + "ext-openssl": "*", "symfony/http-client": "^6.4|^7.0", - "symfony/notifier": "^6.4|^7.0", - "adhocore/jwt": "^1.1" + "symfony/notifier": "^6.4|^7.0" }, "autoload": { "psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Firebase\\": "" }, From 73525c980589a95343c0720994a5b80bde7e1710 Mon Sep 17 00:00:00 2001 From: Cesur APAYDIN Date: Sat, 6 Jan 2024 00:01:03 +0300 Subject: [PATCH 3/9] Update src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseJwtTransport.php Co-authored-by: Oskar Stark --- .../Component/Notifier/Bridge/Firebase/FirebaseJwtTransport.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseJwtTransport.php b/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseJwtTransport.php index f9d4a3fb8b4f8..2b13d83717272 100644 --- a/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseJwtTransport.php +++ b/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseJwtTransport.php @@ -27,7 +27,7 @@ */ final class FirebaseJwtTransport extends AbstractTransport { - protected const HOST = "fcm.googleapis.com/v1/projects/project_id/messages:send"; + protected const HOST = 'fcm.googleapis.com/v1/projects/project_id/messages:send'; private array $credentials; From ec115939b2a89b3567822e1b8b39ce8f02c6ceb1 Mon Sep 17 00:00:00 2001 From: Cesur APAYDIN Date: Sat, 6 Jan 2024 00:01:17 +0300 Subject: [PATCH 4/9] Update src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseJwtTransport.php Co-authored-by: Oskar Stark --- .../Component/Notifier/Bridge/Firebase/FirebaseJwtTransport.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseJwtTransport.php b/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseJwtTransport.php index 2b13d83717272..4ea5fae6178fa 100644 --- a/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseJwtTransport.php +++ b/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseJwtTransport.php @@ -124,7 +124,7 @@ private function getJwtToken(): string return $header . '.' . $payload . '.' . $signature; } - protected function urlSafeEncode($data): string + protected function urlSafeEncode(string|array $data): string { if (is_array($data)) { $data = json_encode($data, JSON_UNESCAPED_SLASHES); From b035895178d3afeb1172c123186a03d5728bda4a Mon Sep 17 00:00:00 2001 From: Cesur APAYDIN Date: Tue, 9 Jul 2024 22:06:36 +0300 Subject: [PATCH 5/9] Removed Lagacy API & DSN Optimized --- .../Notifier/Bridge/Firebase/CHANGELOG.md | 6 + .../Bridge/Firebase/FirebaseJwtTransport.php | 135 ------------------ .../Bridge/Firebase/FirebaseOptions.php | 15 +- .../Bridge/Firebase/FirebaseTransport.php | 58 ++++++-- .../Firebase/FirebaseTransportFactory.php | 30 ++-- .../Notifier/Bridge/Firebase/README.md | 24 ++-- .../Tests/FirebaseJwtTransportFactoryTest.php | 46 ------ .../Tests/FirebaseJwtTransportTest.php | 84 ----------- .../Tests/FirebaseTransportFactoryTest.php | 10 +- .../Firebase/Tests/FirebaseTransportTest.php | 11 +- 10 files changed, 96 insertions(+), 323 deletions(-) delete mode 100644 src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseJwtTransport.php delete mode 100644 src/Symfony/Component/Notifier/Bridge/Firebase/Tests/FirebaseJwtTransportFactoryTest.php delete mode 100644 src/Symfony/Component/Notifier/Bridge/Firebase/Tests/FirebaseJwtTransportTest.php diff --git a/src/Symfony/Component/Notifier/Bridge/Firebase/CHANGELOG.md b/src/Symfony/Component/Notifier/Bridge/Firebase/CHANGELOG.md index 5b5417f3c604a..ce0aa2935d989 100644 --- a/src/Symfony/Component/Notifier/Bridge/Firebase/CHANGELOG.md +++ b/src/Symfony/Component/Notifier/Bridge/Firebase/CHANGELOG.md @@ -1,6 +1,12 @@ CHANGELOG ========= +7.1 +--- + +* The legacy api has been replaced with HTTP v1 +* Add `useTopic` field to options + 5.3 --- diff --git a/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseJwtTransport.php b/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseJwtTransport.php deleted file mode 100644 index 4ea5fae6178fa..0000000000000 --- a/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseJwtTransport.php +++ /dev/null @@ -1,135 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Notifier\Bridge\Firebase; - -use Symfony\Component\Notifier\Exception\InvalidArgumentException; -use Symfony\Component\Notifier\Exception\TransportException; -use Symfony\Component\Notifier\Exception\UnsupportedMessageTypeException; -use Symfony\Component\Notifier\Message\ChatMessage; -use Symfony\Component\Notifier\Message\MessageInterface; -use Symfony\Component\Notifier\Message\SentMessage; -use Symfony\Component\Notifier\Transport\AbstractTransport; -use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; -use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; -use Symfony\Contracts\HttpClient\HttpClientInterface; - -/** - * @author Cesur APAYDIN - */ -final class FirebaseJwtTransport extends AbstractTransport -{ - protected const HOST = 'fcm.googleapis.com/v1/projects/project_id/messages:send'; - - private array $credentials; - - public function __construct(#[\SensitiveParameter] array $credentials, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null) - { - $this->credentials = $credentials; - $this->client = $client; - - $this->setHost(str_replace('project_id', $credentials['project_id'], $this->getDefaultHost())); - - parent::__construct($client, $dispatcher); - } - - public function __toString(): string - { - return sprintf('firebase-jwt://%s', $this->getEndpoint()); - } - - public function supports(MessageInterface $message): bool - { - return $message instanceof ChatMessage && (null === $message->getOptions() || $message->getOptions() instanceof FirebaseOptions); - } - - protected function doSend(MessageInterface $message): SentMessage - { - if (!$message instanceof ChatMessage) { - throw new UnsupportedMessageTypeException(__CLASS__, ChatMessage::class, $message); - } - - $endpoint = sprintf('https://%s', $this->getEndpoint()); - $options = $message->getOptions()?->toArray() ?? []; - $options['token'] = $message->getRecipientId(); - unset($options['to']); - - if (!$options['token']) { - throw new InvalidArgumentException(sprintf('The "%s" transport required the "to" option to be set.', __CLASS__)); - } - $options['notification']['body'] = $message->getSubject(); - $options['data'] ??= []; - - // Send - $response = $this->client->request('POST', $endpoint, [ - 'headers' => [ - 'Authorization' => sprintf('Bearer %s', $this->getJwtToken()), - ], - 'json' => array_filter(['message' => $options]), - ]); - - try { - $statusCode = $response->getStatusCode(); - } catch (TransportExceptionInterface $e) { - throw new TransportException('Could not reach the remote Firebase server.', $response, 0, $e); - } - - $contentType = $response->getHeaders(false)['content-type'][0] ?? ''; - $jsonContents = str_starts_with($contentType, 'application/json') ? $response->toArray(false) : null; - $errorMessage = null; - - if ($jsonContents && isset($jsonContents['results'][0]['error'])) { - $errorMessage = $jsonContents['results'][0]['error']; - } elseif (200 !== $statusCode) { - $errorMessage = $response->getContent(false); - } - - if (null !== $errorMessage) { - throw new TransportException('Unable to post the Firebase message: ' . $errorMessage, $response); - } - - $success = $response->toArray(false); - - $sentMessage = new SentMessage($message, (string)$this); - $sentMessage->setMessageId($success['results'][0]['message_id'] ?? ''); - - return $sentMessage; - } - - private function getJwtToken(): string - { - $time = time(); - $payload = [ - 'iss' => $this->credentials['client_email'], - 'sub' => $this->credentials['client_email'], - 'aud' => 'https://fcm.googleapis.com/', - 'iat' => $time, - 'exp' => $time + 3600, - 'kid' => $this->credentials['private_key_id'], - ]; - - $header = $this->urlSafeEncode(['alg' => 'RS256', 'typ' => 'JWT']); - $payload = $this->urlSafeEncode($payload); - openssl_sign($header . '.' . $payload, $signature, openssl_pkey_get_private($this->credentials['private_key']), OPENSSL_ALGO_SHA256); - $signature = $this->urlSafeEncode($signature); - - return $header . '.' . $payload . '.' . $signature; - } - - protected function urlSafeEncode(string|array $data): string - { - if (is_array($data)) { - $data = json_encode($data, JSON_UNESCAPED_SLASHES); - } - - return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); - } -} diff --git a/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseOptions.php b/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseOptions.php index 728a85a387a5e..f717fa32038be 100644 --- a/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseOptions.php +++ b/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseOptions.php @@ -20,7 +20,7 @@ */ abstract class FirebaseOptions implements MessageOptionsInterface { - private string $to; + private string $tokenOrTopic; /** * @see https://firebase.google.com/docs/cloud-messaging/xmpp-server-ref.html#notification-payload-support @@ -29,25 +29,28 @@ abstract class FirebaseOptions implements MessageOptionsInterface private array $data; - public function __construct(string $to, array $options, array $data = []) + private bool $useTopic; + + public function __construct(string $tokenOrTopic, array $options, array $data = [], bool $useTopic = false) { - $this->to = $to; + $this->tokenOrTopic = $tokenOrTopic; $this->options = $options; $this->data = $data; + $this->useTopic = $useTopic; } public function toArray(): array { return [ - 'to' => $this->to, + ($this->useTopic ? 'topic' : 'token') => $this->tokenOrTopic, 'notification' => $this->options, - 'data' => $this->data, + 'data' => $this->data ]; } public function getRecipientId(): ?string { - return $this->to; + return $this->tokenOrTopic; } /** diff --git a/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseTransport.php b/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseTransport.php index 5ba7f6ae6641d..cf31a921f595d 100644 --- a/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseTransport.php +++ b/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseTransport.php @@ -24,17 +24,19 @@ /** * @author Jeroen Spee + * @author Cesur APAYDIN */ final class FirebaseTransport extends AbstractTransport { - protected const HOST = 'fcm.googleapis.com/fcm/send'; + protected const HOST = 'fcm.googleapis.com/v1/projects/project_id/messages:send'; - private string $token; + private array $credentials; - public function __construct(#[\SensitiveParameter] string $token, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null) + public function __construct(#[\SensitiveParameter] array $credentials, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null) { - $this->token = $token; + $this->credentials = $credentials; $this->client = $client; + $this->setHost(str_replace('project_id', $credentials['project_id'], $this->getDefaultHost())); parent::__construct($client, $dispatcher); } @@ -56,20 +58,19 @@ protected function doSend(MessageInterface $message): SentMessage } $endpoint = sprintf('https://%s', $this->getEndpoint()); - $options = $message->getOptions()?->toArray() ?? []; - $options['to'] = $message->getRecipientId(); - if (!$options['to']) { - throw new InvalidArgumentException(sprintf('The "%s" transport required the "to" option to be set.', __CLASS__)); + // Generate Options + $options = $message->getOptions()?->toArray() ?? []; + if (!$options['token'] && !$options['topic']) { + throw new InvalidArgumentException(sprintf('The "%s" transport required the "token" or "topic" option to be set.', __CLASS__)); } $options['notification']['body'] = $message->getSubject(); $options['data'] ??= []; + // Send $response = $this->client->request('POST', $endpoint, [ - 'headers' => [ - 'Authorization' => sprintf('key=%s', $this->token), - ], - 'json' => array_filter($options), + 'headers' => ['Authorization' => sprintf('Bearer %s', $this->getJwtToken())], + 'json' => array_filter(['message' => $options]), ]); try { @@ -89,14 +90,43 @@ protected function doSend(MessageInterface $message): SentMessage } if (null !== $errorMessage) { - throw new TransportException('Unable to post the Firebase message: '.$errorMessage, $response); + throw new TransportException('Unable to post the Firebase message: ' . $errorMessage, $response); } $success = $response->toArray(false); - $sentMessage = new SentMessage($message, (string) $this); + $sentMessage = new SentMessage($message, (string)$this); $sentMessage->setMessageId($success['results'][0]['message_id'] ?? ''); return $sentMessage; } + + private function getJwtToken(): string + { + $time = time(); + $payload = [ + 'iss' => $this->credentials['client_email'], + 'sub' => $this->credentials['client_email'], + 'aud' => 'https://fcm.googleapis.com/', + 'iat' => $time, + 'exp' => $time + 3600, + 'kid' => $this->credentials['private_key_id'], + ]; + + $header = $this->urlSafeEncode(['alg' => 'RS256', 'typ' => 'JWT']); + $payload = $this->urlSafeEncode($payload); + openssl_sign($header . '.' . $payload, $signature, openssl_pkey_get_private($this->credentials['private_key']), OPENSSL_ALGO_SHA256); + $signature = $this->urlSafeEncode($signature); + + return $header . '.' . $payload . '.' . $signature; + } + + protected function urlSafeEncode(string|array $data): string + { + if (is_array($data)) { + $data = json_encode($data, JSON_UNESCAPED_SLASHES); + } + + return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); + } } diff --git a/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseTransportFactory.php b/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseTransportFactory.php index 5bd2845500b2f..4d150ea28468e 100644 --- a/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseTransportFactory.php +++ b/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseTransportFactory.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Notifier\Bridge\Firebase; +use Symfony\Component\Notifier\Exception\MissingRequiredOptionException; use Symfony\Component\Notifier\Exception\UnsupportedSchemeException; use Symfony\Component\Notifier\Transport\AbstractTransportFactory; use Symfony\Component\Notifier\Transport\Dsn; @@ -20,36 +21,29 @@ */ final class FirebaseTransportFactory extends AbstractTransportFactory { - public function create(Dsn $dsn): FirebaseTransport|FirebaseJwtTransport + public function create(Dsn $dsn): FirebaseTransport { $scheme = $dsn->getScheme(); - if ('firebase-jwt' === $scheme) { - return $this->createJwt($dsn); - } if ('firebase' !== $scheme) { throw new UnsupportedSchemeException($dsn, 'firebase', $this->getSupportedSchemes()); } - $token = sprintf('%s:%s', $this->getUser($dsn), $this->getPassword($dsn)); - $host = 'default' === $dsn->getHost() ? null : $dsn->getHost(); - $port = $dsn->getPort(); - - return (new FirebaseTransport($token, $this->client, $this->dispatcher))->setHost($host)->setPort($port); - } + $credentials = [ + 'client_email' => sprintf('%s@%s', $dsn->getUser(), $dsn->getHost()), + ...$dsn->getOptions() + ]; - public function createJwt(Dsn $dsn): FirebaseJwtTransport - { - $credentials = match ($this->getUser($dsn)) { - 'credentials_path' => file_get_contents($this->getPassword($dsn)), - 'credentials_content' => base64_decode($this->getPassword($dsn)), - }; + $requiredParameters = array_diff(array_keys($credentials), ['client_email', 'project_id', 'private_key_id', 'private_key']); + if ($requiredParameters) { + throw new MissingRequiredOptionException(implode(', ', $requiredParameters)); + } - return (new FirebaseJwtTransport(json_decode($credentials, true, 512, JSON_THROW_ON_ERROR), $this->client, $this->dispatcher)); + return (new FirebaseTransport($credentials, $this->client, $this->dispatcher)); } protected function getSupportedSchemes(): array { - return ['firebase', 'firebase-jwt']; + return ['firebase']; } } diff --git a/src/Symfony/Component/Notifier/Bridge/Firebase/README.md b/src/Symfony/Component/Notifier/Bridge/Firebase/README.md index 85faf04ece19d..d248d422b8eb8 100644 --- a/src/Symfony/Component/Notifier/Bridge/Firebase/README.md +++ b/src/Symfony/Component/Notifier/Bridge/Firebase/README.md @@ -3,25 +3,25 @@ Firebase Notifier Provides [Firebase](https://firebase.google.com) integration for Symfony Notifier. -Legacy DSN example +JWT DSN Example (HTTP v1) ----------- ``` -FIREBASE_DSN=firebase://USERNAME:PASSWORD@default +FIREBASE_DSN=firebase://?project_id=&private_key_id=&private_key= +FIREBASE_DSN=firebase://firebase-adminsdk@stag.iam.gserviceaccount.com?project_id=&private_key_id=&private_key= ``` -where: -- `USERNAME` is your Firebase username -- `PASSWORD` is your Firebase password - -JWT DSN example (HTTP v1) ------------ - +Since __"private_key"__ is long, you must write it in a single line with "\n". Example: ``` -FIREBASE_DSN=firebase-jwt://credentials_path:@default -FIREBASE_DSN=firebase-jwt://credentials_content:@default +-----BEGIN RSA PRIVATE KEY-----\n.....\n....\n-----END RSA PRIVATE KEY----- ``` +__Required Options:__ +* client_email +* project_id +* private_key_id +* private_key + Adding Interactions to a Message -------------------------------- @@ -36,7 +36,7 @@ use Symfony\Component\Notifier\Bridge\Firebase\Notification\AndroidNotification; $chatMessage = new ChatMessage(''); // Create AndroidNotification options -$androidOptions = (new AndroidNotification('/topics/news', [])) +$androidOptions = (new AndroidNotification('/topics/news', [], [], true)) ->icon('myicon') ->sound('default') ->tag('myNotificationId') diff --git a/src/Symfony/Component/Notifier/Bridge/Firebase/Tests/FirebaseJwtTransportFactoryTest.php b/src/Symfony/Component/Notifier/Bridge/Firebase/Tests/FirebaseJwtTransportFactoryTest.php deleted file mode 100644 index 9000480830990..0000000000000 --- a/src/Symfony/Component/Notifier/Bridge/Firebase/Tests/FirebaseJwtTransportFactoryTest.php +++ /dev/null @@ -1,46 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Notifier\Bridge\Firebase\Tests; - -use Symfony\Component\Notifier\Bridge\Firebase\FirebaseTransportFactory; -use Symfony\Component\Notifier\Test\TransportFactoryTestCase; - -/** - * @author Cesur APAYDIN - */ -final class FirebaseJwtTransportFactoryTest extends TransportFactoryTestCase -{ - public function createFactory(): FirebaseTransportFactory - { - return new FirebaseTransportFactory(); - } - - public static function createProvider(): iterable - { - yield [ - 'firebase-jwt://fcm.googleapis.com/v1/projects/test_project/messages:send', - 'firebase-jwt://credentials_content:ewogICJ0eXBlIjogIiIsCiAgInByb2plY3RfaWQiOiAidGVzdF9wcm9qZWN0Igp9Cg==@default', - ]; - } - - public static function supportsProvider(): iterable - { - yield [true, 'firebase-jwt://credentials_path:crendentials.json@default']; - yield [true, 'firebase-jwt://credentials_content:base64Content@default']; - yield [false, 'somethingElse://username:password@default']; - } - - public static function unsupportedSchemeProvider(): iterable - { - yield ['somethingElse://username:password@default']; - } -} diff --git a/src/Symfony/Component/Notifier/Bridge/Firebase/Tests/FirebaseJwtTransportTest.php b/src/Symfony/Component/Notifier/Bridge/Firebase/Tests/FirebaseJwtTransportTest.php deleted file mode 100644 index a1c6c87c30fa4..0000000000000 --- a/src/Symfony/Component/Notifier/Bridge/Firebase/Tests/FirebaseJwtTransportTest.php +++ /dev/null @@ -1,84 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Notifier\Bridge\Firebase\Tests; - -use Symfony\Component\HttpClient\MockHttpClient; -use Symfony\Component\HttpClient\Response\MockResponse; -use Symfony\Component\Notifier\Bridge\Firebase\FirebaseJwtTransport; -use Symfony\Component\Notifier\Bridge\Firebase\FirebaseOptions; -use Symfony\Component\Notifier\Exception\TransportException; -use Symfony\Component\Notifier\Message\ChatMessage; -use Symfony\Component\Notifier\Message\SmsMessage; -use Symfony\Component\Notifier\Test\TransportTestCase; -use Symfony\Component\Notifier\Tests\Transport\DummyMessage; -use Symfony\Contracts\HttpClient\HttpClientInterface; -use Symfony\Contracts\HttpClient\ResponseInterface; - -/** - * @author Cesur APAYDIN - */ -final class FirebaseJwtTransportTest extends TransportTestCase -{ - public static function createTransport(HttpClientInterface $client = null): FirebaseJwtTransport - { - return new FirebaseJwtTransport([ - 'project_id' => 'test_project', - 'client_email' => 'firebase-adminsdk-test@test.iam.gserviceaccount.com', - 'private_key_id' => 'sdas7d6a8ds6ds78a', - 'private_key' => "-----BEGIN RSA PRIVATE KEY-----\nMIICWwIBAAKBgGN4fgq4BFQwjK7kzWUYSFE1ryGIBtUScY5TqLY2BAROBnZS+SIa\nH4VcZJStPUwjtsVxJTf57slhMM5FbAOQkWFMmRlHGWc7EZy6UMMvP8FD21X3Ty9e\nZzJ/Be30la1Uy7rechBh3RN+Y3rSKV+gDmsjdo5/4Jekj4LfluDXbwVJAgMBAAEC\ngYA5SqY2IEUGBKyS81/F8ZV9iNElHAbrZGMZWeAbisMHg7U/I40w8iDjnBKme52J\npCxaTk/kjMTXIm6M7/lFmFfTHgl5WLCimu2glMyKFM2GBYX/cKx9RnI36q3uJYml\n1G1f2H7ALurisenEqMaq8bdyApd/XNqcijogfsZ1K/irTQJBAKEQFkqNDgwUgAwr\njhG/zppl5yEJtP+Pncp/2t/s6khk0q8N92xw6xl8OV/ww+rwlJB3IKVKw903LztQ\nP1D3zpMCQQCeGlOvMx9XxiktNIkdXekGP/bFUR9/u0ABaYl9valZ2B3yZzujJJHV\n0EtyKGorT39wWhWY7BI8NTYgivCIWGozAkEAhMnOlwhUXIFKUL5YEyogHAuH0yU9\npLWzUhC3U4bwYV8+lDTfmPg/3HMemorV/Az9b13H/H73nJqyxiQTD54/IQJAZUX/\n7O4WWac5oRdR7VnGdpZqgCJixvMvILh1tfHTlRV2uVufO/Wk5Q00BsAUogGeZF2Q\nEBDH7YE4VsgpI21fOQJAJdSB7mHvStlYCQMEAYWCWjk+NRW8fzZCkQkqzOV6b9dw\nDFp6wp8aLw87hAHUz5zXTCRYi/BpvDhfP6DDT2sOaw==\n-----END RSA PRIVATE KEY-----" - ], $client ?? new MockHttpClient()); - } - - public static function toStringProvider(): iterable - { - yield ['firebase-jwt://fcm.googleapis.com/v1/projects/test_project/messages:send', self::createTransport()]; - } - - public static function supportedMessagesProvider(): iterable - { - yield [new ChatMessage('Hello!')]; - } - - public static function unsupportedMessagesProvider(): iterable - { - yield [new SmsMessage('0611223344', 'Hello!')]; - yield [new DummyMessage()]; - } - - /** - * @dataProvider sendWithErrorThrowsExceptionProvider - */ - public function testSendWithErrorThrowsTransportException(ResponseInterface $response) - { - $this->expectException(TransportException::class); - - $client = new MockHttpClient(static fn (): ResponseInterface => $response); - $options = new class('recipient-id', []) extends FirebaseOptions {}; - - $transport = self::createTransport($client); - - $transport->send(new ChatMessage('Hello!', $options)); - } - - public static function sendWithErrorThrowsExceptionProvider(): iterable - { - yield [new MockResponse( - json_encode(['results' => [['error' => 'testErrorCode']]]), - ['response_headers' => ['content-type' => ['application/json']], 'http_code' => 200] - )]; - - yield [new MockResponse( - json_encode(['results' => [['error' => 'testErrorCode']]]), - ['response_headers' => ['content-type' => ['application/json']], 'http_code' => 400] - )]; - } -} diff --git a/src/Symfony/Component/Notifier/Bridge/Firebase/Tests/FirebaseTransportFactoryTest.php b/src/Symfony/Component/Notifier/Bridge/Firebase/Tests/FirebaseTransportFactoryTest.php index ed67b6e39deff..36d8f0f9139ec 100644 --- a/src/Symfony/Component/Notifier/Bridge/Firebase/Tests/FirebaseTransportFactoryTest.php +++ b/src/Symfony/Component/Notifier/Bridge/Firebase/Tests/FirebaseTransportFactoryTest.php @@ -27,19 +27,19 @@ public function createFactory(): FirebaseTransportFactory public static function createProvider(): iterable { yield [ - 'firebase://host.test', - 'firebase://username:password@host.test', + 'firebase://fcm.googleapis.com/v1/projects//messages:send', + 'firebase://firebase-adminsdk@stag.iam.gserviceaccount.com?project_id=&private_key_id=&private_key=', ]; } public static function supportsProvider(): iterable { - yield [true, 'firebase://username:password@default']; - yield [false, 'somethingElse://username:password@default']; + yield [true, 'firebase://client_email?project_id=1']; + yield [false, 'somethingElse://client_email?project_id=1']; } public static function unsupportedSchemeProvider(): iterable { - yield ['somethingElse://username:password@default']; + yield ['somethingElse://client_email']; } } diff --git a/src/Symfony/Component/Notifier/Bridge/Firebase/Tests/FirebaseTransportTest.php b/src/Symfony/Component/Notifier/Bridge/Firebase/Tests/FirebaseTransportTest.php index e0124ba35f931..13dfbd9c11eb6 100644 --- a/src/Symfony/Component/Notifier/Bridge/Firebase/Tests/FirebaseTransportTest.php +++ b/src/Symfony/Component/Notifier/Bridge/Firebase/Tests/FirebaseTransportTest.php @@ -30,12 +30,17 @@ final class FirebaseTransportTest extends TransportTestCase { public static function createTransport(HttpClientInterface $client = null): FirebaseTransport { - return new FirebaseTransport('username:password', $client ?? new MockHttpClient()); + return new FirebaseTransport([ + 'client_email' => 'firebase-adminsdk-test@test.iam.gserviceaccount.com', + 'project_id' => 'test_project', + 'private_key_id' => 'sdas7d6a8ds6ds78a', + 'private_key' => "-----BEGIN RSA PRIVATE KEY-----\nMIICWwIBAAKBgGN4fgq4BFQwjK7kzWUYSFE1ryGIBtUScY5TqLY2BAROBnZS+SIa\nH4VcZJStPUwjtsVxJTf57slhMM5FbAOQkWFMmRlHGWc7EZy6UMMvP8FD21X3Ty9e\nZzJ/Be30la1Uy7rechBh3RN+Y3rSKV+gDmsjdo5/4Jekj4LfluDXbwVJAgMBAAEC\ngYA5SqY2IEUGBKyS81/F8ZV9iNElHAbrZGMZWeAbisMHg7U/I40w8iDjnBKme52J\npCxaTk/kjMTXIm6M7/lFmFfTHgl5WLCimu2glMyKFM2GBYX/cKx9RnI36q3uJYml\n1G1f2H7ALurisenEqMaq8bdyApd/XNqcijogfsZ1K/irTQJBAKEQFkqNDgwUgAwr\njhG/zppl5yEJtP+Pncp/2t/s6khk0q8N92xw6xl8OV/ww+rwlJB3IKVKw903LztQ\nP1D3zpMCQQCeGlOvMx9XxiktNIkdXekGP/bFUR9/u0ABaYl9valZ2B3yZzujJJHV\n0EtyKGorT39wWhWY7BI8NTYgivCIWGozAkEAhMnOlwhUXIFKUL5YEyogHAuH0yU9\npLWzUhC3U4bwYV8+lDTfmPg/3HMemorV/Az9b13H/H73nJqyxiQTD54/IQJAZUX/\n7O4WWac5oRdR7VnGdpZqgCJixvMvILh1tfHTlRV2uVufO/Wk5Q00BsAUogGeZF2Q\nEBDH7YE4VsgpI21fOQJAJdSB7mHvStlYCQMEAYWCWjk+NRW8fzZCkQkqzOV6b9dw\nDFp6wp8aLw87hAHUz5zXTCRYi/BpvDhfP6DDT2sOaw==\n-----END RSA PRIVATE KEY-----" + ], $client ?? new MockHttpClient()); } public static function toStringProvider(): iterable { - yield ['firebase://fcm.googleapis.com/fcm/send', self::createTransport()]; + yield ['firebase://fcm.googleapis.com/v1/projects/test_project/messages:send', self::createTransport()]; } public static function supportedMessagesProvider(): iterable @@ -56,7 +61,7 @@ public function testSendWithErrorThrowsTransportException(ResponseInterface $res { $this->expectException(TransportException::class); - $client = new MockHttpClient(static fn (): ResponseInterface => $response); + $client = new MockHttpClient(static fn(): ResponseInterface => $response); $options = new class('recipient-id', []) extends FirebaseOptions {}; $transport = self::createTransport($client); From 6957bd4af82c3fbd6eed05e08f94826b9d682ed2 Mon Sep 17 00:00:00 2001 From: Cesur APAYDIN Date: Tue, 9 Jul 2024 22:32:17 +0300 Subject: [PATCH 6/9] Update src/Symfony/Component/Notifier/Bridge/Firebase/CHANGELOG.md Co-authored-by: Christian Flothmann --- src/Symfony/Component/Notifier/Bridge/Firebase/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/Notifier/Bridge/Firebase/CHANGELOG.md b/src/Symfony/Component/Notifier/Bridge/Firebase/CHANGELOG.md index ce0aa2935d989..a23166d9a5118 100644 --- a/src/Symfony/Component/Notifier/Bridge/Firebase/CHANGELOG.md +++ b/src/Symfony/Component/Notifier/Bridge/Firebase/CHANGELOG.md @@ -1,7 +1,7 @@ CHANGELOG ========= -7.1 +7.2 --- * The legacy api has been replaced with HTTP v1 From a068ca4abc1ee2d69265e0d64fc32b230b710968 Mon Sep 17 00:00:00 2001 From: Cesur APAYDIN Date: Wed, 10 Jul 2024 01:25:14 +0300 Subject: [PATCH 7/9] Env Special Chars Encode --- .../Notifier/Bridge/Firebase/FirebaseTransport.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseTransport.php b/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseTransport.php index cf31a921f595d..66b230f43ea29 100644 --- a/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseTransport.php +++ b/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseTransport.php @@ -115,7 +115,7 @@ private function getJwtToken(): string $header = $this->urlSafeEncode(['alg' => 'RS256', 'typ' => 'JWT']); $payload = $this->urlSafeEncode($payload); - openssl_sign($header . '.' . $payload, $signature, openssl_pkey_get_private($this->credentials['private_key']), OPENSSL_ALGO_SHA256); + openssl_sign($header . '.' . $payload, $signature, openssl_pkey_get_private($this->encodePk($this->credentials['private_key'])), OPENSSL_ALGO_SHA256); $signature = $this->urlSafeEncode($signature); return $header . '.' . $payload . '.' . $signature; @@ -129,4 +129,9 @@ protected function urlSafeEncode(string|array $data): string return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); } + + protected function encodePk(string $privateKey): string + { + return str_replace(['_', ' ', 'BEGIN+PRIVATE+KEY', 'END+PRIVATE+KEY'], ["\n", '+', 'BEGIN PRIVATE KEY', 'END PRIVATE KEY'], $privateKey); + } } From 0ff2696512016d7e19686d8c42d6a7450bb89ddd Mon Sep 17 00:00:00 2001 From: Cesur APAYDIN Date: Wed, 10 Jul 2024 02:29:56 +0300 Subject: [PATCH 8/9] Fixed Specials Chaharacter --- .../Component/Notifier/Bridge/Firebase/FirebaseTransport.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseTransport.php b/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseTransport.php index 47074e30e2ee2..aeaa41bd05c80 100644 --- a/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseTransport.php +++ b/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseTransport.php @@ -132,6 +132,6 @@ protected function urlSafeEncode(string|array $data): string protected function encodePk(string $privateKey): string { - return str_replace(['_', ' ', 'BEGIN+PRIVATE+KEY', 'END+PRIVATE+KEY'], ["\n", '+', 'BEGIN PRIVATE KEY', 'END PRIVATE KEY'], $privateKey); + return str_replace(['\n', '_', ' ', 'BEGIN+PRIVATE+KEY', 'END+PRIVATE+KEY'], ["\n", "\n", '+', 'BEGIN PRIVATE KEY', 'END PRIVATE KEY'], $privateKey); } } From 47075c1446cdcb74d57154dee722235713a82e29 Mon Sep 17 00:00:00 2001 From: Cesur APAYDIN Date: Wed, 10 Jul 2024 04:06:44 +0300 Subject: [PATCH 9/9] Fix String Replace --- .../Component/Notifier/Bridge/Firebase/FirebaseTransport.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseTransport.php b/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseTransport.php index aeaa41bd05c80..6530dae350b66 100644 --- a/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseTransport.php +++ b/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseTransport.php @@ -132,6 +132,9 @@ protected function urlSafeEncode(string|array $data): string protected function encodePk(string $privateKey): string { - return str_replace(['\n', '_', ' ', 'BEGIN+PRIVATE+KEY', 'END+PRIVATE+KEY'], ["\n", "\n", '+', 'BEGIN PRIVATE KEY', 'END PRIVATE KEY'], $privateKey); + $text = explode('-----', $privateKey); + $text[2] = str_replace(['\n', '_', ' '], ["\n", "\n", '+'], $text[2]); + + return implode('-----', $text); } }