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); } } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy