summaryrefslogtreecommitdiffstats
path: root/vendor/minishlink/web-push
diff options
context:
space:
mode:
authorAnton Luka Šijanec <anton@sijanec.eu>2024-05-27 13:08:29 +0200
committerAnton Luka Šijanec <anton@sijanec.eu>2024-05-27 13:08:29 +0200
commit75160b12821f7f4299cce7f0b69c83c1502ae071 (patch)
tree27e25e4ccaef45f0c58b22831164050d1af1d4db /vendor/minishlink/web-push
parentprvi-commit (diff)
download1ka-75160b12821f7f4299cce7f0b69c83c1502ae071.tar
1ka-75160b12821f7f4299cce7f0b69c83c1502ae071.tar.gz
1ka-75160b12821f7f4299cce7f0b69c83c1502ae071.tar.bz2
1ka-75160b12821f7f4299cce7f0b69c83c1502ae071.tar.lz
1ka-75160b12821f7f4299cce7f0b69c83c1502ae071.tar.xz
1ka-75160b12821f7f4299cce7f0b69c83c1502ae071.tar.zst
1ka-75160b12821f7f4299cce7f0b69c83c1502ae071.zip
Diffstat (limited to '')
-rw-r--r--vendor/minishlink/web-push/composer.json78
-rw-r--r--vendor/minishlink/web-push/src/Encryption.php642
-rw-r--r--vendor/minishlink/web-push/src/MessageSentReport.php362
-rw-r--r--vendor/minishlink/web-push/src/Notification.php172
-rw-r--r--vendor/minishlink/web-push/src/Subscription.php244
-rw-r--r--vendor/minishlink/web-push/src/SubscriptionInterface.php80
-rw-r--r--vendor/minishlink/web-push/src/Utils.php126
-rw-r--r--vendor/minishlink/web-push/src/VAPID.php394
-rw-r--r--vendor/minishlink/web-push/src/WebPush.php824
9 files changed, 1461 insertions, 1461 deletions
diff --git a/vendor/minishlink/web-push/composer.json b/vendor/minishlink/web-push/composer.json
index c03a696..8645a53 100644
--- a/vendor/minishlink/web-push/composer.json
+++ b/vendor/minishlink/web-push/composer.json
@@ -1,39 +1,39 @@
-{
- "name": "minishlink/web-push",
- "type": "library",
- "description": "Web Push library for PHP",
- "keywords": ["push", "notifications", "web", "WebPush", "Push API"],
- "homepage": "https://github.com/web-push-libs/web-push-php",
- "license": "MIT",
- "authors": [
- {
- "name": "Louis Lagrange",
- "email": "lagrange.louis@gmail.com",
- "homepage": "https://github.com/Minishlink"
- }
- ],
- "scripts": {
- "test:unit": "./vendor/bin/phpunit --color",
- "test:typing": "./vendor/bin/phpstan analyse --level max src",
- "test:syntax": "./vendor/bin/php-cs-fixer fix ./src --dry-run --stop-on-violation --using-cache=no"
- },
- "require": {
- "php": "^7.1",
- "ext-json": "*",
- "ext-gmp": "*",
- "lib-openssl": "*",
- "guzzlehttp/guzzle": "^6.2",
- "web-token/jwt-signature": "^1.0",
- "web-token/jwt-key-mgmt": "^1.0"
- },
- "require-dev": {
- "phpunit/phpunit": "^7.0",
- "phpstan/phpstan": "0.11.2",
- "friendsofphp/php-cs-fixer": "^2.14"
- },
- "autoload": {
- "psr-4" : {
- "Minishlink\\WebPush\\" : "src"
- }
- }
-}
+{
+ "name": "minishlink/web-push",
+ "type": "library",
+ "description": "Web Push library for PHP",
+ "keywords": ["push", "notifications", "web", "WebPush", "Push API"],
+ "homepage": "https://github.com/web-push-libs/web-push-php",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Louis Lagrange",
+ "email": "lagrange.louis@gmail.com",
+ "homepage": "https://github.com/Minishlink"
+ }
+ ],
+ "scripts": {
+ "test:unit": "./vendor/bin/phpunit --color",
+ "test:typing": "./vendor/bin/phpstan analyse --level max src",
+ "test:syntax": "./vendor/bin/php-cs-fixer fix ./src --dry-run --stop-on-violation --using-cache=no"
+ },
+ "require": {
+ "php": "^7.1",
+ "ext-json": "*",
+ "ext-gmp": "*",
+ "lib-openssl": "*",
+ "guzzlehttp/guzzle": "^6.2",
+ "web-token/jwt-signature": "^1.0",
+ "web-token/jwt-key-mgmt": "^1.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^7.0",
+ "phpstan/phpstan": "0.11.2",
+ "friendsofphp/php-cs-fixer": "^2.14"
+ },
+ "autoload": {
+ "psr-4" : {
+ "Minishlink\\WebPush\\" : "src"
+ }
+ }
+}
diff --git a/vendor/minishlink/web-push/src/Encryption.php b/vendor/minishlink/web-push/src/Encryption.php
index e9fe1ac..c867265 100644
--- a/vendor/minishlink/web-push/src/Encryption.php
+++ b/vendor/minishlink/web-push/src/Encryption.php
@@ -1,321 +1,321 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * This file is part of the WebPush library.
- *
- * (c) Louis Lagrange <lagrange.louis@gmail.com>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Minishlink\WebPush;
-
-use Base64Url\Base64Url;
-use Jose\Component\Core\Util\Ecc\NistCurve;
-use Jose\Component\Core\Util\Ecc\Point;
-use Jose\Component\Core\Util\Ecc\PrivateKey;
-use Jose\Component\Core\Util\Ecc\PublicKey;
-
-class Encryption
-{
- public const MAX_PAYLOAD_LENGTH = 4078;
- public const MAX_COMPATIBILITY_PAYLOAD_LENGTH = 3052;
-
- /**
- * @param string $payload
- * @param int $maxLengthToPad
- * @param string $contentEncoding
- * @return string padded payload (plaintext)
- * @throws \ErrorException
- */
- public static function padPayload(string $payload, int $maxLengthToPad, string $contentEncoding): string
- {
- $payloadLen = Utils::safeStrlen($payload);
- $padLen = $maxLengthToPad ? $maxLengthToPad - $payloadLen : 0;
-
- if ($contentEncoding === "aesgcm") {
- return pack('n*', $padLen).str_pad($payload, $padLen + $payloadLen, chr(0), STR_PAD_LEFT);
- } elseif ($contentEncoding === "aes128gcm") {
- return str_pad($payload.chr(2), $padLen + $payloadLen, chr(0), STR_PAD_RIGHT);
- } else {
- throw new \ErrorException("This content encoding is not supported");
- }
- }
-
- /**
- * @param string $payload With padding
- * @param string $userPublicKey Base 64 encoded (MIME or URL-safe)
- * @param string $userAuthToken Base 64 encoded (MIME or URL-safe)
- * @param string $contentEncoding
- * @return array
- *
- * @throws \ErrorException
- */
- public static function encrypt(string $payload, string $userPublicKey, string $userAuthToken, string $contentEncoding): array
- {
- return self::deterministicEncrypt(
- $payload,
- $userPublicKey,
- $userAuthToken,
- $contentEncoding,
- self::createLocalKeyObject(),
- random_bytes(16)
- );
- }
-
- /**
- * @param string $payload
- * @param string $userPublicKey
- * @param string $userAuthToken
- * @param string $contentEncoding
- * @param array $localKeyObject
- * @param string $salt
- * @return array
- *
- * @throws \ErrorException
- */
- public static function deterministicEncrypt(string $payload, string $userPublicKey, string $userAuthToken, string $contentEncoding, array $localKeyObject, string $salt): array
- {
- $userPublicKey = Base64Url::decode($userPublicKey);
- $userAuthToken = Base64Url::decode($userAuthToken);
-
- $curve = NistCurve::curve256();
-
- // get local key pair
- list($localPublicKeyObject, $localPrivateKeyObject) = $localKeyObject;
- $localPublicKey = hex2bin(Utils::serializePublicKey($localPublicKeyObject));
- if (!$localPublicKey) {
- throw new \ErrorException('Failed to convert local public key from hexadecimal to binary');
- }
-
- // get user public key object
- [$userPublicKeyObjectX, $userPublicKeyObjectY] = Utils::unserializePublicKey($userPublicKey);
- $userPublicKeyObject = $curve->getPublicKeyFrom(
- gmp_init(bin2hex($userPublicKeyObjectX), 16),
- gmp_init(bin2hex($userPublicKeyObjectY), 16)
- );
-
- // get shared secret from user public key and local private key
- $sharedSecret = $curve->mul($userPublicKeyObject->getPoint(), $localPrivateKeyObject->getSecret())->getX();
- $sharedSecret = hex2bin(str_pad(gmp_strval($sharedSecret, 16), 64, '0', STR_PAD_LEFT));
- if (!$sharedSecret) {
- throw new \ErrorException('Failed to convert shared secret from hexadecimal to binary');
- }
-
- // section 4.3
- $ikm = self::getIKM($userAuthToken, $userPublicKey, $localPublicKey, $sharedSecret, $contentEncoding);
-
- // section 4.2
- $context = self::createContext($userPublicKey, $localPublicKey, $contentEncoding);
-
- // derive the Content Encryption Key
- $contentEncryptionKeyInfo = self::createInfo($contentEncoding, $context, $contentEncoding);
- $contentEncryptionKey = self::hkdf($salt, $ikm, $contentEncryptionKeyInfo, 16);
-
- // section 3.3, derive the nonce
- $nonceInfo = self::createInfo('nonce', $context, $contentEncoding);
- $nonce = self::hkdf($salt, $ikm, $nonceInfo, 12);
-
- // encrypt
- // "The additional data passed to each invocation of AEAD_AES_128_GCM is a zero-length octet sequence."
- $tag = '';
- $encryptedText = openssl_encrypt($payload, 'aes-128-gcm', $contentEncryptionKey, OPENSSL_RAW_DATA, $nonce, $tag);
-
- // return values in url safe base64
- return [
- 'localPublicKey' => $localPublicKey,
- 'salt' => $salt,
- 'cipherText' => $encryptedText.$tag,
- ];
- }
-
- public static function getContentCodingHeader($salt, $localPublicKey, $contentEncoding): string
- {
- if ($contentEncoding === "aes128gcm") {
- return $salt
- .pack('N*', 4096)
- .pack('C*', Utils::safeStrlen($localPublicKey))
- .$localPublicKey;
- }
-
- return "";
- }
-
- /**
- * HMAC-based Extract-and-Expand Key Derivation Function (HKDF).
- *
- * This is used to derive a secure encryption key from a mostly-secure shared
- * secret.
- *
- * This is a partial implementation of HKDF tailored to our specific purposes.
- * In particular, for us the value of N will always be 1, and thus T always
- * equals HMAC-Hash(PRK, info | 0x01).
- *
- * See {@link https://www.rfc-editor.org/rfc/rfc5869.txt}
- * From {@link https://github.com/GoogleChrome/push-encryption-node/blob/master/src/encrypt.js}
- *
- * @param string $salt A non-secret random value
- * @param string $ikm Input keying material
- * @param string $info Application-specific context
- * @param int $length The length (in bytes) of the required output key
- *
- * @return string
- */
- private static function hkdf(string $salt, string $ikm, string $info, int $length): string
- {
- // extract
- $prk = hash_hmac('sha256', $ikm, $salt, true);
-
- // expand
- return mb_substr(hash_hmac('sha256', $info.chr(1), $prk, true), 0, $length, '8bit');
- }
-
- /**
- * Creates a context for deriving encryption parameters.
- * See section 4.2 of
- * {@link https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-00}
- * From {@link https://github.com/GoogleChrome/push-encryption-node/blob/master/src/encrypt.js}.
- *
- * @param string $clientPublicKey The client's public key
- * @param string $serverPublicKey Our public key
- *
- * @return null|string
- *
- * @throws \ErrorException
- */
- private static function createContext(string $clientPublicKey, string $serverPublicKey, $contentEncoding): ?string
- {
- if ($contentEncoding === "aes128gcm") {
- return null;
- }
-
- if (Utils::safeStrlen($clientPublicKey) !== 65) {
- throw new \ErrorException('Invalid client public key length');
- }
-
- // This one should never happen, because it's our code that generates the key
- if (Utils::safeStrlen($serverPublicKey) !== 65) {
- throw new \ErrorException('Invalid server public key length');
- }
-
- $len = chr(0).'A'; // 65 as Uint16BE
-
- return chr(0).$len.$clientPublicKey.$len.$serverPublicKey;
- }
-
- /**
- * Returns an info record. See sections 3.2 and 3.3 of
- * {@link https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-00}
- * From {@link https://github.com/GoogleChrome/push-encryption-node/blob/master/src/encrypt.js}.
- *
- * @param string $type The type of the info record
- * @param string|null $context The context for the record
- * @param string $contentEncoding
- * @return string
- *
- * @throws \ErrorException
- */
- private static function createInfo(string $type, ?string $context, string $contentEncoding): string
- {
- if ($contentEncoding === "aesgcm") {
- if (!$context) {
- throw new \ErrorException('Context must exist');
- }
-
- if (Utils::safeStrlen($context) !== 135) {
- throw new \ErrorException('Context argument has invalid size');
- }
-
- return 'Content-Encoding: '.$type.chr(0).'P-256'.$context;
- } elseif ($contentEncoding === "aes128gcm") {
- return 'Content-Encoding: '.$type.chr(0);
- }
-
- throw new \ErrorException('This content encoding is not supported.');
- }
-
- /**
- * @return array
- */
- private static function createLocalKeyObject(): array
- {
- try {
- return self::createLocalKeyObjectUsingOpenSSL();
- } catch (\Exception $e) {
- return self::createLocalKeyObjectUsingPurePhpMethod();
- }
- }
-
- /**
- * @return array
- */
- private static function createLocalKeyObjectUsingPurePhpMethod(): array
- {
- $curve = NistCurve::curve256();
- $privateKey = $curve->createPrivateKey();
-
- return [
- $curve->createPublicKey($privateKey),
- $privateKey,
- ];
- }
-
- /**
- * @return array
- */
- private static function createLocalKeyObjectUsingOpenSSL(): array
- {
- $keyResource = openssl_pkey_new([
- 'curve_name' => 'prime256v1',
- 'private_key_type' => OPENSSL_KEYTYPE_EC,
- ]);
-
- if (!$keyResource) {
- throw new \RuntimeException('Unable to create the key');
- }
-
- $details = openssl_pkey_get_details($keyResource);
- openssl_pkey_free($keyResource);
-
- if (!$details) {
- throw new \RuntimeException('Unable to get the key details');
- }
-
- return [
- PublicKey::create(Point::create(
- gmp_init(bin2hex($details['ec']['x']), 16),
- gmp_init(bin2hex($details['ec']['y']), 16)
- )),
- PrivateKey::create(gmp_init(bin2hex($details['ec']['d']), 16))
- ];
- }
-
- /**
- * @param string $userAuthToken
- * @param string $userPublicKey
- * @param string $localPublicKey
- * @param string $sharedSecret
- * @param string $contentEncoding
- * @return string
- * @throws \ErrorException
- */
- private static function getIKM(string $userAuthToken, string $userPublicKey, string $localPublicKey, string $sharedSecret, string $contentEncoding): string
- {
- if (!empty($userAuthToken)) {
- if ($contentEncoding === "aesgcm") {
- $info = 'Content-Encoding: auth'.chr(0);
- } elseif ($contentEncoding === "aes128gcm") {
- $info = "WebPush: info".chr(0).$userPublicKey.$localPublicKey;
- } else {
- throw new \ErrorException("This content encoding is not supported");
- }
-
- return self::hkdf($userAuthToken, $sharedSecret, $info, 32);
- }
-
- return $sharedSecret;
- }
-}
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the WebPush library.
+ *
+ * (c) Louis Lagrange <lagrange.louis@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Minishlink\WebPush;
+
+use Base64Url\Base64Url;
+use Jose\Component\Core\Util\Ecc\NistCurve;
+use Jose\Component\Core\Util\Ecc\Point;
+use Jose\Component\Core\Util\Ecc\PrivateKey;
+use Jose\Component\Core\Util\Ecc\PublicKey;
+
+class Encryption
+{
+ public const MAX_PAYLOAD_LENGTH = 4078;
+ public const MAX_COMPATIBILITY_PAYLOAD_LENGTH = 3052;
+
+ /**
+ * @param string $payload
+ * @param int $maxLengthToPad
+ * @param string $contentEncoding
+ * @return string padded payload (plaintext)
+ * @throws \ErrorException
+ */
+ public static function padPayload(string $payload, int $maxLengthToPad, string $contentEncoding): string
+ {
+ $payloadLen = Utils::safeStrlen($payload);
+ $padLen = $maxLengthToPad ? $maxLengthToPad - $payloadLen : 0;
+
+ if ($contentEncoding === "aesgcm") {
+ return pack('n*', $padLen).str_pad($payload, $padLen + $payloadLen, chr(0), STR_PAD_LEFT);
+ } elseif ($contentEncoding === "aes128gcm") {
+ return str_pad($payload.chr(2), $padLen + $payloadLen, chr(0), STR_PAD_RIGHT);
+ } else {
+ throw new \ErrorException("This content encoding is not supported");
+ }
+ }
+
+ /**
+ * @param string $payload With padding
+ * @param string $userPublicKey Base 64 encoded (MIME or URL-safe)
+ * @param string $userAuthToken Base 64 encoded (MIME or URL-safe)
+ * @param string $contentEncoding
+ * @return array
+ *
+ * @throws \ErrorException
+ */
+ public static function encrypt(string $payload, string $userPublicKey, string $userAuthToken, string $contentEncoding): array
+ {
+ return self::deterministicEncrypt(
+ $payload,
+ $userPublicKey,
+ $userAuthToken,
+ $contentEncoding,
+ self::createLocalKeyObject(),
+ random_bytes(16)
+ );
+ }
+
+ /**
+ * @param string $payload
+ * @param string $userPublicKey
+ * @param string $userAuthToken
+ * @param string $contentEncoding
+ * @param array $localKeyObject
+ * @param string $salt
+ * @return array
+ *
+ * @throws \ErrorException
+ */
+ public static function deterministicEncrypt(string $payload, string $userPublicKey, string $userAuthToken, string $contentEncoding, array $localKeyObject, string $salt): array
+ {
+ $userPublicKey = Base64Url::decode($userPublicKey);
+ $userAuthToken = Base64Url::decode($userAuthToken);
+
+ $curve = NistCurve::curve256();
+
+ // get local key pair
+ list($localPublicKeyObject, $localPrivateKeyObject) = $localKeyObject;
+ $localPublicKey = hex2bin(Utils::serializePublicKey($localPublicKeyObject));
+ if (!$localPublicKey) {
+ throw new \ErrorException('Failed to convert local public key from hexadecimal to binary');
+ }
+
+ // get user public key object
+ [$userPublicKeyObjectX, $userPublicKeyObjectY] = Utils::unserializePublicKey($userPublicKey);
+ $userPublicKeyObject = $curve->getPublicKeyFrom(
+ gmp_init(bin2hex($userPublicKeyObjectX), 16),
+ gmp_init(bin2hex($userPublicKeyObjectY), 16)
+ );
+
+ // get shared secret from user public key and local private key
+ $sharedSecret = $curve->mul($userPublicKeyObject->getPoint(), $localPrivateKeyObject->getSecret())->getX();
+ $sharedSecret = hex2bin(str_pad(gmp_strval($sharedSecret, 16), 64, '0', STR_PAD_LEFT));
+ if (!$sharedSecret) {
+ throw new \ErrorException('Failed to convert shared secret from hexadecimal to binary');
+ }
+
+ // section 4.3
+ $ikm = self::getIKM($userAuthToken, $userPublicKey, $localPublicKey, $sharedSecret, $contentEncoding);
+
+ // section 4.2
+ $context = self::createContext($userPublicKey, $localPublicKey, $contentEncoding);
+
+ // derive the Content Encryption Key
+ $contentEncryptionKeyInfo = self::createInfo($contentEncoding, $context, $contentEncoding);
+ $contentEncryptionKey = self::hkdf($salt, $ikm, $contentEncryptionKeyInfo, 16);
+
+ // section 3.3, derive the nonce
+ $nonceInfo = self::createInfo('nonce', $context, $contentEncoding);
+ $nonce = self::hkdf($salt, $ikm, $nonceInfo, 12);
+
+ // encrypt
+ // "The additional data passed to each invocation of AEAD_AES_128_GCM is a zero-length octet sequence."
+ $tag = '';
+ $encryptedText = openssl_encrypt($payload, 'aes-128-gcm', $contentEncryptionKey, OPENSSL_RAW_DATA, $nonce, $tag);
+
+ // return values in url safe base64
+ return [
+ 'localPublicKey' => $localPublicKey,
+ 'salt' => $salt,
+ 'cipherText' => $encryptedText.$tag,
+ ];
+ }
+
+ public static function getContentCodingHeader($salt, $localPublicKey, $contentEncoding): string
+ {
+ if ($contentEncoding === "aes128gcm") {
+ return $salt
+ .pack('N*', 4096)
+ .pack('C*', Utils::safeStrlen($localPublicKey))
+ .$localPublicKey;
+ }
+
+ return "";
+ }
+
+ /**
+ * HMAC-based Extract-and-Expand Key Derivation Function (HKDF).
+ *
+ * This is used to derive a secure encryption key from a mostly-secure shared
+ * secret.
+ *
+ * This is a partial implementation of HKDF tailored to our specific purposes.
+ * In particular, for us the value of N will always be 1, and thus T always
+ * equals HMAC-Hash(PRK, info | 0x01).
+ *
+ * See {@link https://www.rfc-editor.org/rfc/rfc5869.txt}
+ * From {@link https://github.com/GoogleChrome/push-encryption-node/blob/master/src/encrypt.js}
+ *
+ * @param string $salt A non-secret random value
+ * @param string $ikm Input keying material
+ * @param string $info Application-specific context
+ * @param int $length The length (in bytes) of the required output key
+ *
+ * @return string
+ */
+ private static function hkdf(string $salt, string $ikm, string $info, int $length): string
+ {
+ // extract
+ $prk = hash_hmac('sha256', $ikm, $salt, true);
+
+ // expand
+ return mb_substr(hash_hmac('sha256', $info.chr(1), $prk, true), 0, $length, '8bit');
+ }
+
+ /**
+ * Creates a context for deriving encryption parameters.
+ * See section 4.2 of
+ * {@link https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-00}
+ * From {@link https://github.com/GoogleChrome/push-encryption-node/blob/master/src/encrypt.js}.
+ *
+ * @param string $clientPublicKey The client's public key
+ * @param string $serverPublicKey Our public key
+ *
+ * @return null|string
+ *
+ * @throws \ErrorException
+ */
+ private static function createContext(string $clientPublicKey, string $serverPublicKey, $contentEncoding): ?string
+ {
+ if ($contentEncoding === "aes128gcm") {
+ return null;
+ }
+
+ if (Utils::safeStrlen($clientPublicKey) !== 65) {
+ throw new \ErrorException('Invalid client public key length');
+ }
+
+ // This one should never happen, because it's our code that generates the key
+ if (Utils::safeStrlen($serverPublicKey) !== 65) {
+ throw new \ErrorException('Invalid server public key length');
+ }
+
+ $len = chr(0).'A'; // 65 as Uint16BE
+
+ return chr(0).$len.$clientPublicKey.$len.$serverPublicKey;
+ }
+
+ /**
+ * Returns an info record. See sections 3.2 and 3.3 of
+ * {@link https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-00}
+ * From {@link https://github.com/GoogleChrome/push-encryption-node/blob/master/src/encrypt.js}.
+ *
+ * @param string $type The type of the info record
+ * @param string|null $context The context for the record
+ * @param string $contentEncoding
+ * @return string
+ *
+ * @throws \ErrorException
+ */
+ private static function createInfo(string $type, ?string $context, string $contentEncoding): string
+ {
+ if ($contentEncoding === "aesgcm") {
+ if (!$context) {
+ throw new \ErrorException('Context must exist');
+ }
+
+ if (Utils::safeStrlen($context) !== 135) {
+ throw new \ErrorException('Context argument has invalid size');
+ }
+
+ return 'Content-Encoding: '.$type.chr(0).'P-256'.$context;
+ } elseif ($contentEncoding === "aes128gcm") {
+ return 'Content-Encoding: '.$type.chr(0);
+ }
+
+ throw new \ErrorException('This content encoding is not supported.');
+ }
+
+ /**
+ * @return array
+ */
+ private static function createLocalKeyObject(): array
+ {
+ try {
+ return self::createLocalKeyObjectUsingOpenSSL();
+ } catch (\Exception $e) {
+ return self::createLocalKeyObjectUsingPurePhpMethod();
+ }
+ }
+
+ /**
+ * @return array
+ */
+ private static function createLocalKeyObjectUsingPurePhpMethod(): array
+ {
+ $curve = NistCurve::curve256();
+ $privateKey = $curve->createPrivateKey();
+
+ return [
+ $curve->createPublicKey($privateKey),
+ $privateKey,
+ ];
+ }
+
+ /**
+ * @return array
+ */
+ private static function createLocalKeyObjectUsingOpenSSL(): array
+ {
+ $keyResource = openssl_pkey_new([
+ 'curve_name' => 'prime256v1',
+ 'private_key_type' => OPENSSL_KEYTYPE_EC,
+ ]);
+
+ if (!$keyResource) {
+ throw new \RuntimeException('Unable to create the key');
+ }
+
+ $details = openssl_pkey_get_details($keyResource);
+ openssl_pkey_free($keyResource);
+
+ if (!$details) {
+ throw new \RuntimeException('Unable to get the key details');
+ }
+
+ return [
+ PublicKey::create(Point::create(
+ gmp_init(bin2hex($details['ec']['x']), 16),
+ gmp_init(bin2hex($details['ec']['y']), 16)
+ )),
+ PrivateKey::create(gmp_init(bin2hex($details['ec']['d']), 16))
+ ];
+ }
+
+ /**
+ * @param string $userAuthToken
+ * @param string $userPublicKey
+ * @param string $localPublicKey
+ * @param string $sharedSecret
+ * @param string $contentEncoding
+ * @return string
+ * @throws \ErrorException
+ */
+ private static function getIKM(string $userAuthToken, string $userPublicKey, string $localPublicKey, string $sharedSecret, string $contentEncoding): string
+ {
+ if (!empty($userAuthToken)) {
+ if ($contentEncoding === "aesgcm") {
+ $info = 'Content-Encoding: auth'.chr(0);
+ } elseif ($contentEncoding === "aes128gcm") {
+ $info = "WebPush: info".chr(0).$userPublicKey.$localPublicKey;
+ } else {
+ throw new \ErrorException("This content encoding is not supported");
+ }
+
+ return self::hkdf($userAuthToken, $sharedSecret, $info, 32);
+ }
+
+ return $sharedSecret;
+ }
+}
diff --git a/vendor/minishlink/web-push/src/MessageSentReport.php b/vendor/minishlink/web-push/src/MessageSentReport.php
index c569952..a6945e6 100644
--- a/vendor/minishlink/web-push/src/MessageSentReport.php
+++ b/vendor/minishlink/web-push/src/MessageSentReport.php
@@ -1,181 +1,181 @@
-<?php
-/**
- * @author Igor Timoshenkov [it@campoint.net]
- * @started: 03.09.2018 9:21
- */
-
-namespace Minishlink\WebPush;
-
-use Psr\Http\Message\RequestInterface;
-use Psr\Http\Message\ResponseInterface;
-
-/**
- * Standardized response from sending a message
- */
-class MessageSentReport implements \JsonSerializable
-{
-
- /**
- * @var boolean
- */
- protected $success;
-
- /**
- * @var RequestInterface
- */
- protected $request;
-
- /**
- * @var ResponseInterface | null
- */
- protected $response;
-
- /**
- * @var string
- */
- protected $reason;
-
- /**
- * @param RequestInterface $request
- * @param ResponseInterface $response
- * @param bool $success
- * @param string $reason
- */
- public function __construct(RequestInterface $request, ?ResponseInterface $response = null, bool $success = true, $reason = 'OK')
- {
- $this->request = $request;
- $this->response = $response;
- $this->success = $success;
- $this->reason = $reason;
- }
-
- /**
- * @return bool
- */
- public function isSuccess(): bool
- {
- return $this->success;
- }
-
- /**
- * @param bool $success
- *
- * @return MessageSentReport
- */
- public function setSuccess(bool $success): MessageSentReport
- {
- $this->success = $success;
- return $this;
- }
-
- /**
- * @return RequestInterface
- */
- public function getRequest(): RequestInterface
- {
- return $this->request;
- }
-
- /**
- * @param RequestInterface $request
- *
- * @return MessageSentReport
- */
- public function setRequest(RequestInterface $request): MessageSentReport
- {
- $this->request = $request;
- return $this;
- }
-
- /**
- * @return ResponseInterface | null
- */
- public function getResponse(): ?ResponseInterface
- {
- return $this->response;
- }
-
- /**
- * @param ResponseInterface $response
- *
- * @return MessageSentReport
- */
- public function setResponse(ResponseInterface $response): MessageSentReport
- {
- $this->response = $response;
- return $this;
- }
-
- /**
- * @return string
- */
- public function getEndpoint(): string
- {
- return $this->request->getUri()->__toString();
- }
-
- /**
- * @return bool
- */
- public function isSubscriptionExpired(): bool
- {
- if (!$this->response) {
- return false;
- }
-
- return \in_array($this->response->getStatusCode(), [404, 410], true);
- }
-
- /**
- * @return string
- */
- public function getReason(): string
- {
- return $this->reason;
- }
-
- /**
- * @param string $reason
- *
- * @return MessageSentReport
- */
- public function setReason(string $reason): MessageSentReport
- {
- $this->reason = $reason;
- return $this;
- }
-
- /**
- * @return string
- */
- public function getRequestPayload(): string
- {
- return $this->request->getBody()->getContents();
- }
-
- /**
- * @return string | null
- */
- public function getResponseContent(): ?string
- {
- if (!$this->response) {
- return null;
- }
-
- return $this->response->getBody()->getContents();
- }
-
- /**
- * @return array|mixed
- */
- public function jsonSerialize()
- {
- return [
- 'success' => $this->isSuccess(),
- 'expired' => $this->isSubscriptionExpired(),
- 'reason' => $this->reason,
- 'endpoint' => $this->getEndpoint(),
- 'payload' => $this->request->getBody()->getContents(),
- ];
- }
-}
+<?php
+/**
+ * @author Igor Timoshenkov [it@campoint.net]
+ * @started: 03.09.2018 9:21
+ */
+
+namespace Minishlink\WebPush;
+
+use Psr\Http\Message\RequestInterface;
+use Psr\Http\Message\ResponseInterface;
+
+/**
+ * Standardized response from sending a message
+ */
+class MessageSentReport implements \JsonSerializable
+{
+
+ /**
+ * @var boolean
+ */
+ protected $success;
+
+ /**
+ * @var RequestInterface
+ */
+ protected $request;
+
+ /**
+ * @var ResponseInterface | null
+ */
+ protected $response;
+
+ /**
+ * @var string
+ */
+ protected $reason;
+
+ /**
+ * @param RequestInterface $request
+ * @param ResponseInterface $response
+ * @param bool $success
+ * @param string $reason
+ */
+ public function __construct(RequestInterface $request, ?ResponseInterface $response = null, bool $success = true, $reason = 'OK')
+ {
+ $this->request = $request;
+ $this->response = $response;
+ $this->success = $success;
+ $this->reason = $reason;
+ }
+
+ /**
+ * @return bool
+ */
+ public function isSuccess(): bool
+ {
+ return $this->success;
+ }
+
+ /**
+ * @param bool $success
+ *
+ * @return MessageSentReport
+ */
+ public function setSuccess(bool $success): MessageSentReport
+ {
+ $this->success = $success;
+ return $this;
+ }
+
+ /**
+ * @return RequestInterface
+ */
+ public function getRequest(): RequestInterface
+ {
+ return $this->request;
+ }
+
+ /**
+ * @param RequestInterface $request
+ *
+ * @return MessageSentReport
+ */
+ public function setRequest(RequestInterface $request): MessageSentReport
+ {
+ $this->request = $request;
+ return $this;
+ }
+
+ /**
+ * @return ResponseInterface | null
+ */
+ public function getResponse(): ?ResponseInterface
+ {
+ return $this->response;
+ }
+
+ /**
+ * @param ResponseInterface $response
+ *
+ * @return MessageSentReport
+ */
+ public function setResponse(ResponseInterface $response): MessageSentReport
+ {
+ $this->response = $response;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getEndpoint(): string
+ {
+ return $this->request->getUri()->__toString();
+ }
+
+ /**
+ * @return bool
+ */
+ public function isSubscriptionExpired(): bool
+ {
+ if (!$this->response) {
+ return false;
+ }
+
+ return \in_array($this->response->getStatusCode(), [404, 410], true);
+ }
+
+ /**
+ * @return string
+ */
+ public function getReason(): string
+ {
+ return $this->reason;
+ }
+
+ /**
+ * @param string $reason
+ *
+ * @return MessageSentReport
+ */
+ public function setReason(string $reason): MessageSentReport
+ {
+ $this->reason = $reason;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getRequestPayload(): string
+ {
+ return $this->request->getBody()->getContents();
+ }
+
+ /**
+ * @return string | null
+ */
+ public function getResponseContent(): ?string
+ {
+ if (!$this->response) {
+ return null;
+ }
+
+ return $this->response->getBody()->getContents();
+ }
+
+ /**
+ * @return array|mixed
+ */
+ public function jsonSerialize()
+ {
+ return [
+ 'success' => $this->isSuccess(),
+ 'expired' => $this->isSubscriptionExpired(),
+ 'reason' => $this->reason,
+ 'endpoint' => $this->getEndpoint(),
+ 'payload' => $this->request->getBody()->getContents(),
+ ];
+ }
+}
diff --git a/vendor/minishlink/web-push/src/Notification.php b/vendor/minishlink/web-push/src/Notification.php
index 1107404..15d634f 100644
--- a/vendor/minishlink/web-push/src/Notification.php
+++ b/vendor/minishlink/web-push/src/Notification.php
@@ -1,86 +1,86 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * This file is part of the WebPush library.
- *
- * (c) Louis Lagrange <lagrange.louis@gmail.com>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Minishlink\WebPush;
-
-class Notification
-{
- /** @var SubscriptionInterface */
- private $subscription;
-
- /** @var null|string */
- private $payload;
-
- /** @var array Options : TTL, urgency, topic */
- private $options;
-
- /** @var array Auth details : GCM, VAPID */
- private $auth;
-
- /**
- * Notification constructor.
- *
- * @param SubscriptionInterface $subscription
- * @param null|string $payload
- * @param array $options
- * @param array $auth
- */
- public function __construct(SubscriptionInterface $subscription, ?string $payload, array $options, array $auth)
- {
- $this->subscription = $subscription;
- $this->payload = $payload;
- $this->options = $options;
- $this->auth = $auth;
- }
-
- /**
- * @return SubscriptionInterface
- */
- public function getSubscription(): SubscriptionInterface
- {
- return $this->subscription;
- }
-
- /**
- * @return null|string
- */
- public function getPayload(): ?string
- {
- return $this->payload;
- }
-
- /**
- * @param array $defaultOptions
- *
- * @return array
- */
- public function getOptions(array $defaultOptions = []): array
- {
- $options = $this->options;
- $options['TTL'] = array_key_exists('TTL', $options) ? $options['TTL'] : $defaultOptions['TTL'];
- $options['urgency'] = array_key_exists('urgency', $options) ? $options['urgency'] : $defaultOptions['urgency'];
- $options['topic'] = array_key_exists('topic', $options) ? $options['topic'] : $defaultOptions['topic'];
-
- return $options;
- }
-
- /**
- * @param array $defaultAuth
- *
- * @return array
- */
- public function getAuth(array $defaultAuth): array
- {
- return count($this->auth) > 0 ? $this->auth : $defaultAuth;
- }
-}
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the WebPush library.
+ *
+ * (c) Louis Lagrange <lagrange.louis@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Minishlink\WebPush;
+
+class Notification
+{
+ /** @var SubscriptionInterface */
+ private $subscription;
+
+ /** @var null|string */
+ private $payload;
+
+ /** @var array Options : TTL, urgency, topic */
+ private $options;
+
+ /** @var array Auth details : GCM, VAPID */
+ private $auth;
+
+ /**
+ * Notification constructor.
+ *
+ * @param SubscriptionInterface $subscription
+ * @param null|string $payload
+ * @param array $options
+ * @param array $auth
+ */
+ public function __construct(SubscriptionInterface $subscription, ?string $payload, array $options, array $auth)
+ {
+ $this->subscription = $subscription;
+ $this->payload = $payload;
+ $this->options = $options;
+ $this->auth = $auth;
+ }
+
+ /**
+ * @return SubscriptionInterface
+ */
+ public function getSubscription(): SubscriptionInterface
+ {
+ return $this->subscription;
+ }
+
+ /**
+ * @return null|string
+ */
+ public function getPayload(): ?string
+ {
+ return $this->payload;
+ }
+
+ /**
+ * @param array $defaultOptions
+ *
+ * @return array
+ */
+ public function getOptions(array $defaultOptions = []): array
+ {
+ $options = $this->options;
+ $options['TTL'] = array_key_exists('TTL', $options) ? $options['TTL'] : $defaultOptions['TTL'];
+ $options['urgency'] = array_key_exists('urgency', $options) ? $options['urgency'] : $defaultOptions['urgency'];
+ $options['topic'] = array_key_exists('topic', $options) ? $options['topic'] : $defaultOptions['topic'];
+
+ return $options;
+ }
+
+ /**
+ * @param array $defaultAuth
+ *
+ * @return array
+ */
+ public function getAuth(array $defaultAuth): array
+ {
+ return count($this->auth) > 0 ? $this->auth : $defaultAuth;
+ }
+}
diff --git a/vendor/minishlink/web-push/src/Subscription.php b/vendor/minishlink/web-push/src/Subscription.php
index 1232893..f570c30 100644
--- a/vendor/minishlink/web-push/src/Subscription.php
+++ b/vendor/minishlink/web-push/src/Subscription.php
@@ -1,122 +1,122 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * This file is part of the WebPush library.
- *
- * (c) Louis Lagrange <lagrange.louis@gmail.com>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Minishlink\WebPush;
-
-class Subscription implements SubscriptionInterface
-{
- /** @var string */
- private $endpoint;
-
- /** @var null|string */
- private $publicKey;
-
- /** @var null|string */
- private $authToken;
-
- /** @var null|string */
- private $contentEncoding;
-
- /**
- * Subscription constructor.
- *
- * @param string $endpoint
- * @param null|string $publicKey
- * @param null|string $authToken
- * @param string $contentEncoding (Optional) Must be "aesgcm"
- * @throws \ErrorException
- */
- public function __construct(
- string $endpoint,
- ?string $publicKey = null,
- ?string $authToken = null,
- ?string $contentEncoding = null
- ) {
- $this->endpoint = $endpoint;
-
- if ($publicKey || $authToken || $contentEncoding) {
- $supportedContentEncodings = ['aesgcm', 'aes128gcm'];
- if ($contentEncoding && !in_array($contentEncoding, $supportedContentEncodings)) {
- throw new \ErrorException('This content encoding ('.$contentEncoding.') is not supported.');
- }
-
- $this->publicKey = $publicKey;
- $this->authToken = $authToken;
- $this->contentEncoding = $contentEncoding ?: "aesgcm";
- }
- }
-
- /**
- * Subscription factory.
- *
- * @param array $associativeArray (with keys endpoint, publicKey, authToken, contentEncoding)
- * @return self
- * @throws \ErrorException
- */
- public static function create(array $associativeArray): self
- {
- if (array_key_exists('keys', $associativeArray) && is_array($associativeArray['keys'])) {
- return new self(
- $associativeArray['endpoint'],
- $associativeArray['keys']['p256dh'] ?? null,
- $associativeArray['keys']['auth'] ?? null,
- $associativeArray['contentEncoding'] ?? "aesgcm"
- );
- }
-
- if (array_key_exists('publicKey', $associativeArray) || array_key_exists('authToken', $associativeArray) || array_key_exists('contentEncoding', $associativeArray)) {
- return new self(
- $associativeArray['endpoint'],
- $associativeArray['publicKey'] ?? null,
- $associativeArray['authToken'] ?? null,
- $associativeArray['contentEncoding'] ?? "aesgcm"
- );
- }
-
- return new self(
- $associativeArray['endpoint']
- );
- }
-
- /**
- * {@inheritDoc}
- */
- public function getEndpoint(): string
- {
- return $this->endpoint;
- }
-
- /**
- * {@inheritDoc}
- */
- public function getPublicKey(): ?string
- {
- return $this->publicKey;
- }
-
- /**
- * {@inheritDoc}
- */
- public function getAuthToken(): ?string
- {
- return $this->authToken;
- }
-
- /**
- * {@inheritDoc}
- */
- public function getContentEncoding(): ?string
- {
- return $this->contentEncoding;
- }
-}
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the WebPush library.
+ *
+ * (c) Louis Lagrange <lagrange.louis@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Minishlink\WebPush;
+
+class Subscription implements SubscriptionInterface
+{
+ /** @var string */
+ private $endpoint;
+
+ /** @var null|string */
+ private $publicKey;
+
+ /** @var null|string */
+ private $authToken;
+
+ /** @var null|string */
+ private $contentEncoding;
+
+ /**
+ * Subscription constructor.
+ *
+ * @param string $endpoint
+ * @param null|string $publicKey
+ * @param null|string $authToken
+ * @param string $contentEncoding (Optional) Must be "aesgcm"
+ * @throws \ErrorException
+ */
+ public function __construct(
+ string $endpoint,
+ ?string $publicKey = null,
+ ?string $authToken = null,
+ ?string $contentEncoding = null
+ ) {
+ $this->endpoint = $endpoint;
+
+ if ($publicKey || $authToken || $contentEncoding) {
+ $supportedContentEncodings = ['aesgcm', 'aes128gcm'];
+ if ($contentEncoding && !in_array($contentEncoding, $supportedContentEncodings)) {
+ throw new \ErrorException('This content encoding ('.$contentEncoding.') is not supported.');
+ }
+
+ $this->publicKey = $publicKey;
+ $this->authToken = $authToken;
+ $this->contentEncoding = $contentEncoding ?: "aesgcm";
+ }
+ }
+
+ /**
+ * Subscription factory.
+ *
+ * @param array $associativeArray (with keys endpoint, publicKey, authToken, contentEncoding)
+ * @return self
+ * @throws \ErrorException
+ */
+ public static function create(array $associativeArray): self
+ {
+ if (array_key_exists('keys', $associativeArray) && is_array($associativeArray['keys'])) {
+ return new self(
+ $associativeArray['endpoint'],
+ $associativeArray['keys']['p256dh'] ?? null,
+ $associativeArray['keys']['auth'] ?? null,
+ $associativeArray['contentEncoding'] ?? "aesgcm"
+ );
+ }
+
+ if (array_key_exists('publicKey', $associativeArray) || array_key_exists('authToken', $associativeArray) || array_key_exists('contentEncoding', $associativeArray)) {
+ return new self(
+ $associativeArray['endpoint'],
+ $associativeArray['publicKey'] ?? null,
+ $associativeArray['authToken'] ?? null,
+ $associativeArray['contentEncoding'] ?? "aesgcm"
+ );
+ }
+
+ return new self(
+ $associativeArray['endpoint']
+ );
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getEndpoint(): string
+ {
+ return $this->endpoint;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getPublicKey(): ?string
+ {
+ return $this->publicKey;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getAuthToken(): ?string
+ {
+ return $this->authToken;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getContentEncoding(): ?string
+ {
+ return $this->contentEncoding;
+ }
+}
diff --git a/vendor/minishlink/web-push/src/SubscriptionInterface.php b/vendor/minishlink/web-push/src/SubscriptionInterface.php
index e3f18d6..9e09bed 100644
--- a/vendor/minishlink/web-push/src/SubscriptionInterface.php
+++ b/vendor/minishlink/web-push/src/SubscriptionInterface.php
@@ -1,40 +1,40 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * This file is part of the WebPush library.
- *
- * (c) Louis Lagrange <lagrange.louis@gmail.com>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Minishlink\WebPush;
-
-/**
- * @author Sergii Bondarenko <sb@firstvector.org>
- */
-interface SubscriptionInterface
-{
- /**
- * @return string
- */
- public function getEndpoint(): string;
-
- /**
- * @return null|string
- */
- public function getPublicKey(): ?string;
-
- /**
- * @return null|string
- */
- public function getAuthToken(): ?string;
-
- /**
- * @return null|string
- */
- public function getContentEncoding(): ?string;
-}
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the WebPush library.
+ *
+ * (c) Louis Lagrange <lagrange.louis@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Minishlink\WebPush;
+
+/**
+ * @author Sergii Bondarenko <sb@firstvector.org>
+ */
+interface SubscriptionInterface
+{
+ /**
+ * @return string
+ */
+ public function getEndpoint(): string;
+
+ /**
+ * @return null|string
+ */
+ public function getPublicKey(): ?string;
+
+ /**
+ * @return null|string
+ */
+ public function getAuthToken(): ?string;
+
+ /**
+ * @return null|string
+ */
+ public function getContentEncoding(): ?string;
+}
diff --git a/vendor/minishlink/web-push/src/Utils.php b/vendor/minishlink/web-push/src/Utils.php
index 30c2018..bd7f6c4 100644
--- a/vendor/minishlink/web-push/src/Utils.php
+++ b/vendor/minishlink/web-push/src/Utils.php
@@ -1,63 +1,63 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * This file is part of the WebPush library.
- *
- * (c) Louis Lagrange <lagrange.louis@gmail.com>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Minishlink\WebPush;
-
-use Jose\Component\Core\Util\Ecc\PublicKey;
-
-class Utils
-{
- /**
- * @param string $value
- *
- * @return int
- */
- public static function safeStrlen(string $value): int
- {
- return mb_strlen($value, '8bit');
- }
-
- /**
- * @param PublicKey $publicKey
- *
- * @return string
- */
- public static function serializePublicKey(PublicKey $publicKey): string
- {
- $hexString = '04';
- $hexString .= str_pad(gmp_strval($publicKey->getPoint()->getX(), 16), 64, '0', STR_PAD_LEFT);
- $hexString .= str_pad(gmp_strval($publicKey->getPoint()->getY(), 16), 64, '0', STR_PAD_LEFT);
-
- return $hexString;
- }
-
- /**
- * @param string $data
- *
- * @return array
- */
- public static function unserializePublicKey(string $data): array
- {
- $data = bin2hex($data);
- if (mb_substr($data, 0, 2, '8bit') !== '04') {
- throw new \InvalidArgumentException('Invalid data: only uncompressed keys are supported.');
- }
- $data = mb_substr($data, 2, null, '8bit');
- $dataLength = self::safeStrlen($data);
-
- return [
- hex2bin(mb_substr($data, 0, $dataLength / 2, '8bit')),
- hex2bin(mb_substr($data, $dataLength / 2, null, '8bit')),
- ];
- }
-}
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the WebPush library.
+ *
+ * (c) Louis Lagrange <lagrange.louis@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Minishlink\WebPush;
+
+use Jose\Component\Core\Util\Ecc\PublicKey;
+
+class Utils
+{
+ /**
+ * @param string $value
+ *
+ * @return int
+ */
+ public static function safeStrlen(string $value): int
+ {
+ return mb_strlen($value, '8bit');
+ }
+
+ /**
+ * @param PublicKey $publicKey
+ *
+ * @return string
+ */
+ public static function serializePublicKey(PublicKey $publicKey): string
+ {
+ $hexString = '04';
+ $hexString .= str_pad(gmp_strval($publicKey->getPoint()->getX(), 16), 64, '0', STR_PAD_LEFT);
+ $hexString .= str_pad(gmp_strval($publicKey->getPoint()->getY(), 16), 64, '0', STR_PAD_LEFT);
+
+ return $hexString;
+ }
+
+ /**
+ * @param string $data
+ *
+ * @return array
+ */
+ public static function unserializePublicKey(string $data): array
+ {
+ $data = bin2hex($data);
+ if (mb_substr($data, 0, 2, '8bit') !== '04') {
+ throw new \InvalidArgumentException('Invalid data: only uncompressed keys are supported.');
+ }
+ $data = mb_substr($data, 2, null, '8bit');
+ $dataLength = self::safeStrlen($data);
+
+ return [
+ hex2bin(mb_substr($data, 0, $dataLength / 2, '8bit')),
+ hex2bin(mb_substr($data, $dataLength / 2, null, '8bit')),
+ ];
+ }
+}
diff --git a/vendor/minishlink/web-push/src/VAPID.php b/vendor/minishlink/web-push/src/VAPID.php
index c741ec9..e1f555f 100644
--- a/vendor/minishlink/web-push/src/VAPID.php
+++ b/vendor/minishlink/web-push/src/VAPID.php
@@ -1,197 +1,197 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * This file is part of the WebPush library.
- *
- * (c) Louis Lagrange <lagrange.louis@gmail.com>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Minishlink\WebPush;
-
-use Base64Url\Base64Url;
-use Jose\Component\Core\AlgorithmManager;
-use Jose\Component\Core\Converter\StandardConverter;
-use Jose\Component\Core\JWK;
-use Jose\Component\Core\Util\Ecc\NistCurve;
-use Jose\Component\Core\Util\Ecc\Point;
-use Jose\Component\Core\Util\Ecc\PublicKey;
-use Jose\Component\KeyManagement\JWKFactory;
-use Jose\Component\Signature\Algorithm\ES256;
-use Jose\Component\Signature\JWSBuilder;
-use Jose\Component\Signature\Serializer\CompactSerializer;
-
-class VAPID
-{
- private const PUBLIC_KEY_LENGTH = 65;
- private const PRIVATE_KEY_LENGTH = 32;
-
- /**
- * @param array $vapid
- *
- * @return array
- *
- * @throws \ErrorException
- */
- public static function validate(array $vapid): array
- {
- if (!isset($vapid['subject'])) {
- throw new \ErrorException('[VAPID] You must provide a subject that is either a mailto: or a URL.');
- }
-
- if (isset($vapid['pemFile'])) {
- $vapid['pem'] = file_get_contents($vapid['pemFile']);
-
- if (!$vapid['pem']) {
- throw new \ErrorException('Error loading PEM file.');
- }
- }
-
- if (isset($vapid['pem'])) {
- $jwk = JWKFactory::createFromKey($vapid['pem']);
- if ($jwk->get('kty') !== 'EC' || !$jwk->has('d') || !$jwk->has('x') || !$jwk->has('y')) {
- throw new \ErrorException('Invalid PEM data.');
- }
- $publicKey = PublicKey::create(Point::create(
- gmp_init(bin2hex(Base64Url::decode($jwk->get('x'))), 16),
- gmp_init(bin2hex(Base64Url::decode($jwk->get('y'))), 16)
- ));
-
- $binaryPublicKey = hex2bin(Utils::serializePublicKey($publicKey));
- if (!$binaryPublicKey) {
- throw new \ErrorException('Failed to convert VAPID public key from hexadecimal to binary');
- }
- $vapid['publicKey'] = base64_encode($binaryPublicKey);
- $vapid['privateKey'] = base64_encode(str_pad(Base64Url::decode($jwk->get('d')), 2 * self::PRIVATE_KEY_LENGTH, '0', STR_PAD_LEFT));
- }
-
- if (!isset($vapid['publicKey'])) {
- throw new \ErrorException('[VAPID] You must provide a public key.');
- }
-
- $publicKey = Base64Url::decode($vapid['publicKey']);
-
- if (Utils::safeStrlen($publicKey) !== self::PUBLIC_KEY_LENGTH) {
- throw new \ErrorException('[VAPID] Public key should be 65 bytes long when decoded.');
- }
-
- if (!isset($vapid['privateKey'])) {
- throw new \ErrorException('[VAPID] You must provide a private key.');
- }
-
- $privateKey = Base64Url::decode($vapid['privateKey']);
-
- if (Utils::safeStrlen($privateKey) !== self::PRIVATE_KEY_LENGTH) {
- throw new \ErrorException('[VAPID] Private key should be 32 bytes long when decoded.');
- }
-
- return [
- 'subject' => $vapid['subject'],
- 'publicKey' => $publicKey,
- 'privateKey' => $privateKey,
- ];
- }
-
- /**
- * This method takes the required VAPID parameters and returns the required
- * header to be added to a Web Push Protocol Request.
- *
- * @param string $audience This must be the origin of the push service
- * @param string $subject This should be a URL or a 'mailto:' email address
- * @param string $publicKey The decoded VAPID public key
- * @param string $privateKey The decoded VAPID private key
- * @param string $contentEncoding
- * @param null|int $expiration The expiration of the VAPID JWT. (UNIX timestamp)
- *
- * @return array Returns an array with the 'Authorization' and 'Crypto-Key' values to be used as headers
- * @throws \ErrorException
- */
- public static function getVapidHeaders(string $audience, string $subject, string $publicKey, string $privateKey, string $contentEncoding, ?int $expiration = null)
- {
- $expirationLimit = time() + 43200; // equal margin of error between 0 and 24h
- if (null === $expiration || $expiration > $expirationLimit) {
- $expiration = $expirationLimit;
- }
-
- $header = [
- 'typ' => 'JWT',
- 'alg' => 'ES256',
- ];
-
- $jwtPayload = json_encode([
- 'aud' => $audience,
- 'exp' => $expiration,
- 'sub' => $subject,
- ], JSON_UNESCAPED_SLASHES | JSON_NUMERIC_CHECK);
- if (!$jwtPayload) {
- throw new \ErrorException('Failed to encode JWT payload in JSON');
- }
-
- list($x, $y) = Utils::unserializePublicKey($publicKey);
- $jwk = JWK::create([
- 'kty' => 'EC',
- 'crv' => 'P-256',
- 'x' => Base64Url::encode($x),
- 'y' => Base64Url::encode($y),
- 'd' => Base64Url::encode($privateKey),
- ]);
-
- $jsonConverter = new StandardConverter();
- $jwsCompactSerializer = new CompactSerializer($jsonConverter);
- $jwsBuilder = new JWSBuilder($jsonConverter, AlgorithmManager::create([new ES256()]));
- $jws = $jwsBuilder
- ->create()
- ->withPayload($jwtPayload)
- ->addSignature($jwk, $header)
- ->build();
-
- $jwt = $jwsCompactSerializer->serialize($jws, 0);
- $encodedPublicKey = Base64Url::encode($publicKey);
-
- if ($contentEncoding === "aesgcm") {
- return [
- 'Authorization' => 'WebPush '.$jwt,
- 'Crypto-Key' => 'p256ecdsa='.$encodedPublicKey,
- ];
- } elseif ($contentEncoding === 'aes128gcm') {
- return [
- 'Authorization' => 'vapid t='.$jwt.', k='.$encodedPublicKey,
- ];
- }
-
- throw new \ErrorException('This content encoding is not supported');
- }
-
- /**
- * This method creates VAPID keys in case you would not be able to have a Linux bash.
- * DO NOT create keys at each initialization! Save those keys and reuse them.
- *
- * @return array
- * @throws \ErrorException
- */
- public static function createVapidKeys(): array
- {
- $curve = NistCurve::curve256();
- $privateKey = $curve->createPrivateKey();
- $publicKey = $curve->createPublicKey($privateKey);
-
- $binaryPublicKey = hex2bin(Utils::serializePublicKey($publicKey));
- if (!$binaryPublicKey) {
- throw new \ErrorException('Failed to convert VAPID public key from hexadecimal to binary');
- }
-
- $binaryPrivateKey = hex2bin(str_pad(gmp_strval($privateKey->getSecret(), 16), 2 * self::PRIVATE_KEY_LENGTH, '0', STR_PAD_LEFT));
- if (!$binaryPrivateKey) {
- throw new \ErrorException('Failed to convert VAPID private key from hexadecimal to binary');
- }
-
- return [
- 'publicKey' => Base64Url::encode($binaryPublicKey),
- 'privateKey' => Base64Url::encode($binaryPrivateKey)
- ];
- }
-}
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the WebPush library.
+ *
+ * (c) Louis Lagrange <lagrange.louis@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Minishlink\WebPush;
+
+use Base64Url\Base64Url;
+use Jose\Component\Core\AlgorithmManager;
+use Jose\Component\Core\Converter\StandardConverter;
+use Jose\Component\Core\JWK;
+use Jose\Component\Core\Util\Ecc\NistCurve;
+use Jose\Component\Core\Util\Ecc\Point;
+use Jose\Component\Core\Util\Ecc\PublicKey;
+use Jose\Component\KeyManagement\JWKFactory;
+use Jose\Component\Signature\Algorithm\ES256;
+use Jose\Component\Signature\JWSBuilder;
+use Jose\Component\Signature\Serializer\CompactSerializer;
+
+class VAPID
+{
+ private const PUBLIC_KEY_LENGTH = 65;
+ private const PRIVATE_KEY_LENGTH = 32;
+
+ /**
+ * @param array $vapid
+ *
+ * @return array
+ *
+ * @throws \ErrorException
+ */
+ public static function validate(array $vapid): array
+ {
+ if (!isset($vapid['subject'])) {
+ throw new \ErrorException('[VAPID] You must provide a subject that is either a mailto: or a URL.');
+ }
+
+ if (isset($vapid['pemFile'])) {
+ $vapid['pem'] = file_get_contents($vapid['pemFile']);
+
+ if (!$vapid['pem']) {
+ throw new \ErrorException('Error loading PEM file.');
+ }
+ }
+
+ if (isset($vapid['pem'])) {
+ $jwk = JWKFactory::createFromKey($vapid['pem']);
+ if ($jwk->get('kty') !== 'EC' || !$jwk->has('d') || !$jwk->has('x') || !$jwk->has('y')) {
+ throw new \ErrorException('Invalid PEM data.');
+ }
+ $publicKey = PublicKey::create(Point::create(
+ gmp_init(bin2hex(Base64Url::decode($jwk->get('x'))), 16),
+ gmp_init(bin2hex(Base64Url::decode($jwk->get('y'))), 16)
+ ));
+
+ $binaryPublicKey = hex2bin(Utils::serializePublicKey($publicKey));
+ if (!$binaryPublicKey) {
+ throw new \ErrorException('Failed to convert VAPID public key from hexadecimal to binary');
+ }
+ $vapid['publicKey'] = base64_encode($binaryPublicKey);
+ $vapid['privateKey'] = base64_encode(str_pad(Base64Url::decode($jwk->get('d')), 2 * self::PRIVATE_KEY_LENGTH, '0', STR_PAD_LEFT));
+ }
+
+ if (!isset($vapid['publicKey'])) {
+ throw new \ErrorException('[VAPID] You must provide a public key.');
+ }
+
+ $publicKey = Base64Url::decode($vapid['publicKey']);
+
+ if (Utils::safeStrlen($publicKey) !== self::PUBLIC_KEY_LENGTH) {
+ throw new \ErrorException('[VAPID] Public key should be 65 bytes long when decoded.');
+ }
+
+ if (!isset($vapid['privateKey'])) {
+ throw new \ErrorException('[VAPID] You must provide a private key.');
+ }
+
+ $privateKey = Base64Url::decode($vapid['privateKey']);
+
+ if (Utils::safeStrlen($privateKey) !== self::PRIVATE_KEY_LENGTH) {
+ throw new \ErrorException('[VAPID] Private key should be 32 bytes long when decoded.');
+ }
+
+ return [
+ 'subject' => $vapid['subject'],
+ 'publicKey' => $publicKey,
+ 'privateKey' => $privateKey,
+ ];
+ }
+
+ /**
+ * This method takes the required VAPID parameters and returns the required
+ * header to be added to a Web Push Protocol Request.
+ *
+ * @param string $audience This must be the origin of the push service
+ * @param string $subject This should be a URL or a 'mailto:' email address
+ * @param string $publicKey The decoded VAPID public key
+ * @param string $privateKey The decoded VAPID private key
+ * @param string $contentEncoding
+ * @param null|int $expiration The expiration of the VAPID JWT. (UNIX timestamp)
+ *
+ * @return array Returns an array with the 'Authorization' and 'Crypto-Key' values to be used as headers
+ * @throws \ErrorException
+ */
+ public static function getVapidHeaders(string $audience, string $subject, string $publicKey, string $privateKey, string $contentEncoding, ?int $expiration = null)
+ {
+ $expirationLimit = time() + 43200; // equal margin of error between 0 and 24h
+ if (null === $expiration || $expiration > $expirationLimit) {
+ $expiration = $expirationLimit;
+ }
+
+ $header = [
+ 'typ' => 'JWT',
+ 'alg' => 'ES256',
+ ];
+
+ $jwtPayload = json_encode([
+ 'aud' => $audience,
+ 'exp' => $expiration,
+ 'sub' => $subject,
+ ], JSON_UNESCAPED_SLASHES | JSON_NUMERIC_CHECK);
+ if (!$jwtPayload) {
+ throw new \ErrorException('Failed to encode JWT payload in JSON');
+ }
+
+ list($x, $y) = Utils::unserializePublicKey($publicKey);
+ $jwk = JWK::create([
+ 'kty' => 'EC',
+ 'crv' => 'P-256',
+ 'x' => Base64Url::encode($x),
+ 'y' => Base64Url::encode($y),
+ 'd' => Base64Url::encode($privateKey),
+ ]);
+
+ $jsonConverter = new StandardConverter();
+ $jwsCompactSerializer = new CompactSerializer($jsonConverter);
+ $jwsBuilder = new JWSBuilder($jsonConverter, AlgorithmManager::create([new ES256()]));
+ $jws = $jwsBuilder
+ ->create()
+ ->withPayload($jwtPayload)
+ ->addSignature($jwk, $header)
+ ->build();
+
+ $jwt = $jwsCompactSerializer->serialize($jws, 0);
+ $encodedPublicKey = Base64Url::encode($publicKey);
+
+ if ($contentEncoding === "aesgcm") {
+ return [
+ 'Authorization' => 'WebPush '.$jwt,
+ 'Crypto-Key' => 'p256ecdsa='.$encodedPublicKey,
+ ];
+ } elseif ($contentEncoding === 'aes128gcm') {
+ return [
+ 'Authorization' => 'vapid t='.$jwt.', k='.$encodedPublicKey,
+ ];
+ }
+
+ throw new \ErrorException('This content encoding is not supported');
+ }
+
+ /**
+ * This method creates VAPID keys in case you would not be able to have a Linux bash.
+ * DO NOT create keys at each initialization! Save those keys and reuse them.
+ *
+ * @return array
+ * @throws \ErrorException
+ */
+ public static function createVapidKeys(): array
+ {
+ $curve = NistCurve::curve256();
+ $privateKey = $curve->createPrivateKey();
+ $publicKey = $curve->createPublicKey($privateKey);
+
+ $binaryPublicKey = hex2bin(Utils::serializePublicKey($publicKey));
+ if (!$binaryPublicKey) {
+ throw new \ErrorException('Failed to convert VAPID public key from hexadecimal to binary');
+ }
+
+ $binaryPrivateKey = hex2bin(str_pad(gmp_strval($privateKey->getSecret(), 16), 2 * self::PRIVATE_KEY_LENGTH, '0', STR_PAD_LEFT));
+ if (!$binaryPrivateKey) {
+ throw new \ErrorException('Failed to convert VAPID private key from hexadecimal to binary');
+ }
+
+ return [
+ 'publicKey' => Base64Url::encode($binaryPublicKey),
+ 'privateKey' => Base64Url::encode($binaryPrivateKey)
+ ];
+ }
+}
diff --git a/vendor/minishlink/web-push/src/WebPush.php b/vendor/minishlink/web-push/src/WebPush.php
index aaa9b4b..1f83812 100644
--- a/vendor/minishlink/web-push/src/WebPush.php
+++ b/vendor/minishlink/web-push/src/WebPush.php
@@ -1,412 +1,412 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * This file is part of the WebPush library.
- *
- * (c) Louis Lagrange <lagrange.louis@gmail.com>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Minishlink\WebPush;
-
-use Base64Url\Base64Url;
-use GuzzleHttp\Client;
-use GuzzleHttp\Exception\RequestException;
-use GuzzleHttp\Psr7\Request;
-use Psr\Http\Message\ResponseInterface;
-
-class WebPush
-{
- public const GCM_URL = 'https://android.googleapis.com/gcm/send';
- public const FCM_BASE_URL = 'https://fcm.googleapis.com';
-
- /**
- * @var Client
- */
- private $client;
-
- /**
- * @var array
- */
- private $auth;
-
- /**
- * @var null|array Array of array of Notifications
- */
- private $notifications;
-
- /**
- * @var array Default options : TTL, urgency, topic, batchSize
- */
- private $defaultOptions;
-
- /**
- * @var int Automatic padding of payloads, if disabled, trade security for bandwidth
- */
- private $automaticPadding = Encryption::MAX_COMPATIBILITY_PAYLOAD_LENGTH;
-
- /**
- * @var bool Reuse VAPID headers in the same flush session to improve performance
- */
- private $reuseVAPIDHeaders = false;
-
- /**
- * @var array Dictionary for VAPID headers cache
- */
- private $vapidHeaders = [];
-
- /**
- * WebPush constructor.
- *
- * @param array $auth Some servers needs authentication
- * @param array $defaultOptions TTL, urgency, topic, batchSize
- * @param int|null $timeout Timeout of POST request
- * @param array $clientOptions
- *
- * @throws \ErrorException
- */
- public function __construct(array $auth = [], array $defaultOptions = [], ?int $timeout = 30, array $clientOptions = [])
- {
- $extensions = [
- 'curl' => '[WebPush] curl extension is not loaded but is required. You can fix this in your php.ini.',
- 'gmp' => '[WebPush] gmp extension is not loaded but is required for sending push notifications with payload or for VAPID authentication. You can fix this in your php.ini.',
- 'mbstring' => '[WebPush] mbstring extension is not loaded but is required for sending push notifications with payload or for VAPID authentication. You can fix this in your php.ini.',
- 'openssl' => '[WebPush] openssl extension is not loaded but is required for sending push notifications with payload or for VAPID authentication. You can fix this in your php.ini.',
- ];
- foreach ($extensions as $extension => $message) {
- if (!extension_loaded($extension)) {
- trigger_error($message, E_USER_WARNING);
- }
- }
-
- if (ini_get('mbstring.func_overload') >= 2) {
- trigger_error("[WebPush] mbstring.func_overload is enabled for str* functions. You must disable it if you want to send push notifications with payload or use VAPID. You can fix this in your php.ini.", E_USER_NOTICE);
- }
-
- if (isset($auth['VAPID'])) {
- $auth['VAPID'] = VAPID::validate($auth['VAPID']);
- }
-
- $this->auth = $auth;
-
- $this->setDefaultOptions($defaultOptions);
-
- if (!array_key_exists('timeout', $clientOptions) && isset($timeout)) {
- $clientOptions['timeout'] = $timeout;
- }
- $this->client = new Client($clientOptions);
- }
-
- /**
- * Send a notification.
- *
- * @param SubscriptionInterface $subscription
- * @param string|null $payload If you want to send an array, json_encode it
- * @param bool $flush If you want to flush directly (usually when you send only one notification)
- * @param array $options Array with several options tied to this notification. If not set, will use the default options that you can set in the WebPush object
- * @param array $auth Use this auth details instead of what you provided when creating WebPush
- *
- * @return \Generator|MessageSentReport[]|true Return an array of information if $flush is set to true and the queued requests has failed.
- * Else return true
- *
- * @throws \ErrorException
- */
- public function sendNotification(SubscriptionInterface $subscription, ?string $payload = null, bool $flush = false, array $options = [], array $auth = [])
- {
- if (isset($payload)) {
- if (Utils::safeStrlen($payload) > Encryption::MAX_PAYLOAD_LENGTH) {
- throw new \ErrorException('Size of payload must not be greater than '.Encryption::MAX_PAYLOAD_LENGTH.' octets.');
- }
-
- $contentEncoding = $subscription->getContentEncoding();
- if (!$contentEncoding) {
- throw new \ErrorException('Subscription should have a content encoding');
- }
-
- $payload = Encryption::padPayload($payload, $this->automaticPadding, $contentEncoding);
- }
-
- if (array_key_exists('VAPID', $auth)) {
- $auth['VAPID'] = VAPID::validate($auth['VAPID']);
- }
-
- $this->notifications[] = new Notification($subscription, $payload, $options, $auth);
-
- return false !== $flush ? $this->flush() : true;
- }
-
- /**
- * Flush notifications. Triggers the requests.
- *
- * @param null|int $batchSize Defaults the value defined in defaultOptions during instantiation (which defaults to 1000).
- *
- * @return \Generator|MessageSentReport[]
- * @throws \ErrorException
- */
- public function flush(?int $batchSize = null): \Generator
- {
- if (null === $this->notifications || empty($this->notifications)) {
- yield from [];
- return;
- }
-
- if (null === $batchSize) {
- $batchSize = $this->defaultOptions['batchSize'];
- }
-
- $batches = array_chunk($this->notifications, $batchSize);
-
- // reset queue
- $this->notifications = [];
-
- foreach ($batches as $batch) {
- // for each endpoint server type
- $requests = $this->prepare($batch);
-
- $promises = [];
-
- foreach ($requests as $request) {
- $promises[] = $this->client->sendAsync($request)
- ->then(function ($response) use ($request) {
- /** @var ResponseInterface $response * */
- return new MessageSentReport($request, $response);
- })
- ->otherwise(function ($reason) {
- /** @var RequestException $reason **/
- return new MessageSentReport($reason->getRequest(), $reason->getResponse(), false, $reason->getMessage());
- });
- }
-
- foreach ($promises as $promise) {
- yield $promise->wait();
- }
- }
-
- if ($this->reuseVAPIDHeaders) {
- $this->vapidHeaders = [];
- }
- }
-
- /**
- * @param array $notifications
- *
- * @return array
- *
- * @throws \ErrorException
- */
- private function prepare(array $notifications): array
- {
- $requests = [];
- /** @var Notification $notification */
- foreach ($notifications as $notification) {
- $subscription = $notification->getSubscription();
- $endpoint = $subscription->getEndpoint();
- $userPublicKey = $subscription->getPublicKey();
- $userAuthToken = $subscription->getAuthToken();
- $contentEncoding = $subscription->getContentEncoding();
- $payload = $notification->getPayload();
- $options = $notification->getOptions($this->getDefaultOptions());
- $auth = $notification->getAuth($this->auth);
-
- if (!empty($payload) && !empty($userPublicKey) && !empty($userAuthToken)) {
- if (!$contentEncoding) {
- throw new \ErrorException('Subscription should have a content encoding');
- }
-
- $encrypted = Encryption::encrypt($payload, $userPublicKey, $userAuthToken, $contentEncoding);
- $cipherText = $encrypted['cipherText'];
- $salt = $encrypted['salt'];
- $localPublicKey = $encrypted['localPublicKey'];
-
- $headers = [
- 'Content-Type' => 'application/octet-stream',
- 'Content-Encoding' => $contentEncoding,
- ];
-
- if ($contentEncoding === "aesgcm") {
- $headers['Encryption'] = 'salt='.Base64Url::encode($salt);
- $headers['Crypto-Key'] = 'dh='.Base64Url::encode($localPublicKey);
- }
-
- $encryptionContentCodingHeader = Encryption::getContentCodingHeader($salt, $localPublicKey, $contentEncoding);
- $content = $encryptionContentCodingHeader.$cipherText;
-
- $headers['Content-Length'] = Utils::safeStrlen($content);
- } else {
- $headers = [
- 'Content-Length' => 0,
- ];
-
- $content = '';
- }
-
- $headers['TTL'] = $options['TTL'];
-
- if (isset($options['urgency'])) {
- $headers['Urgency'] = $options['urgency'];
- }
-
- if (isset($options['topic'])) {
- $headers['Topic'] = $options['topic'];
- }
-
- // if GCM
- if (substr($endpoint, 0, strlen(self::GCM_URL)) === self::GCM_URL) {
- if (array_key_exists('GCM', $auth)) {
- $headers['Authorization'] = 'key='.$auth['GCM'];
- } else {
- throw new \ErrorException('No GCM API Key specified.');
- }
- }
- // if VAPID (GCM doesn't support it but FCM does)
- elseif (array_key_exists('VAPID', $auth) && $contentEncoding) {
- $audience = parse_url($endpoint, PHP_URL_SCHEME).'://'.parse_url($endpoint, PHP_URL_HOST);
- if (!parse_url($audience)) {
- throw new \ErrorException('Audience "'.$audience.'"" could not be generated.');
- }
-
- $vapidHeaders = $this->getVAPIDHeaders($audience, $contentEncoding, $auth['VAPID']);
-
- $headers['Authorization'] = $vapidHeaders['Authorization'];
-
- if ($contentEncoding === 'aesgcm') {
- if (array_key_exists('Crypto-Key', $headers)) {
- $headers['Crypto-Key'] .= ';'.$vapidHeaders['Crypto-Key'];
- } else {
- $headers['Crypto-Key'] = $vapidHeaders['Crypto-Key'];
- }
- }
- }
-
- $requests[] = new Request('POST', $endpoint, $headers, $content);
- }
-
- return $requests;
- }
-
- /**
- * @return bool
- */
- public function isAutomaticPadding(): bool
- {
- return $this->automaticPadding !== 0;
- }
-
- /**
- * @return int
- */
- public function getAutomaticPadding()
- {
- return $this->automaticPadding;
- }
-
- /**
- * @param int|bool $automaticPadding Max padding length
- *
- * @return WebPush
- *
- * @throws \Exception
- */
- public function setAutomaticPadding($automaticPadding): WebPush
- {
- if ($automaticPadding > Encryption::MAX_PAYLOAD_LENGTH) {
- throw new \Exception('Automatic padding is too large. Max is '.Encryption::MAX_PAYLOAD_LENGTH.'. Recommended max is '.Encryption::MAX_COMPATIBILITY_PAYLOAD_LENGTH.' for compatibility reasons (see README).');
- } elseif ($automaticPadding < 0) {
- throw new \Exception('Padding length should be positive or zero.');
- } elseif ($automaticPadding === true) {
- $this->automaticPadding = Encryption::MAX_COMPATIBILITY_PAYLOAD_LENGTH;
- } elseif ($automaticPadding === false) {
- $this->automaticPadding = 0;
- } else {
- $this->automaticPadding = $automaticPadding;
- }
-
- return $this;
- }
-
- /**
- * @return bool
- */
- public function getReuseVAPIDHeaders()
- {
- return $this->reuseVAPIDHeaders;
- }
-
- /**
- * Reuse VAPID headers in the same flush session to improve performance
- * @param bool $enabled
- *
- * @return WebPush
- */
- public function setReuseVAPIDHeaders(bool $enabled)
- {
- $this->reuseVAPIDHeaders = $enabled;
-
- return $this;
- }
-
- /**
- * @return array
- */
- public function getDefaultOptions(): array
- {
- return $this->defaultOptions;
- }
-
- /**
- * @param array $defaultOptions Keys 'TTL' (Time To Live, defaults 4 weeks), 'urgency', 'topic', 'batchSize'
- *
- * @return WebPush
- */
- public function setDefaultOptions(array $defaultOptions)
- {
- $this->defaultOptions['TTL'] = isset($defaultOptions['TTL']) ? $defaultOptions['TTL'] : 2419200;
- $this->defaultOptions['urgency'] = isset($defaultOptions['urgency']) ? $defaultOptions['urgency'] : null;
- $this->defaultOptions['topic'] = isset($defaultOptions['topic']) ? $defaultOptions['topic'] : null;
- $this->defaultOptions['batchSize'] = isset($defaultOptions['batchSize']) ? $defaultOptions['batchSize'] : 1000;
-
- return $this;
- }
-
- /**
- * @return int
- */
- public function countPendingNotifications(): int
- {
- return null !== $this->notifications ? count($this->notifications) : 0;
- }
-
- /**
- * @param string $audience
- * @param string $contentEncoding
- * @param array $vapid
- * @return array
- * @throws \ErrorException
- */
- private function getVAPIDHeaders(string $audience, string $contentEncoding, array $vapid)
- {
- $vapidHeaders = null;
-
- $cache_key = null;
- if ($this->reuseVAPIDHeaders) {
- $cache_key = implode('#', [$audience, $contentEncoding, crc32(serialize($vapid))]);
- if (array_key_exists($cache_key, $this->vapidHeaders)) {
- $vapidHeaders = $this->vapidHeaders[$cache_key];
- }
- }
-
- if (!$vapidHeaders) {
- $vapidHeaders = VAPID::getVapidHeaders($audience, $vapid['subject'], $vapid['publicKey'], $vapid['privateKey'], $contentEncoding);
- }
-
- if ($this->reuseVAPIDHeaders) {
- $this->vapidHeaders[$cache_key] = $vapidHeaders;
- }
-
- return $vapidHeaders;
- }
-}
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the WebPush library.
+ *
+ * (c) Louis Lagrange <lagrange.louis@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Minishlink\WebPush;
+
+use Base64Url\Base64Url;
+use GuzzleHttp\Client;
+use GuzzleHttp\Exception\RequestException;
+use GuzzleHttp\Psr7\Request;
+use Psr\Http\Message\ResponseInterface;
+
+class WebPush
+{
+ public const GCM_URL = 'https://android.googleapis.com/gcm/send';
+ public const FCM_BASE_URL = 'https://fcm.googleapis.com';
+
+ /**
+ * @var Client
+ */
+ private $client;
+
+ /**
+ * @var array
+ */
+ private $auth;
+
+ /**
+ * @var null|array Array of array of Notifications
+ */
+ private $notifications;
+
+ /**
+ * @var array Default options : TTL, urgency, topic, batchSize
+ */
+ private $defaultOptions;
+
+ /**
+ * @var int Automatic padding of payloads, if disabled, trade security for bandwidth
+ */
+ private $automaticPadding = Encryption::MAX_COMPATIBILITY_PAYLOAD_LENGTH;
+
+ /**
+ * @var bool Reuse VAPID headers in the same flush session to improve performance
+ */
+ private $reuseVAPIDHeaders = false;
+
+ /**
+ * @var array Dictionary for VAPID headers cache
+ */
+ private $vapidHeaders = [];
+
+ /**
+ * WebPush constructor.
+ *
+ * @param array $auth Some servers needs authentication
+ * @param array $defaultOptions TTL, urgency, topic, batchSize
+ * @param int|null $timeout Timeout of POST request
+ * @param array $clientOptions
+ *
+ * @throws \ErrorException
+ */
+ public function __construct(array $auth = [], array $defaultOptions = [], ?int $timeout = 30, array $clientOptions = [])
+ {
+ $extensions = [
+ 'curl' => '[WebPush] curl extension is not loaded but is required. You can fix this in your php.ini.',
+ 'gmp' => '[WebPush] gmp extension is not loaded but is required for sending push notifications with payload or for VAPID authentication. You can fix this in your php.ini.',
+ 'mbstring' => '[WebPush] mbstring extension is not loaded but is required for sending push notifications with payload or for VAPID authentication. You can fix this in your php.ini.',
+ 'openssl' => '[WebPush] openssl extension is not loaded but is required for sending push notifications with payload or for VAPID authentication. You can fix this in your php.ini.',
+ ];
+ foreach ($extensions as $extension => $message) {
+ if (!extension_loaded($extension)) {
+ trigger_error($message, E_USER_WARNING);
+ }
+ }
+
+ if (ini_get('mbstring.func_overload') >= 2) {
+ trigger_error("[WebPush] mbstring.func_overload is enabled for str* functions. You must disable it if you want to send push notifications with payload or use VAPID. You can fix this in your php.ini.", E_USER_NOTICE);
+ }
+
+ if (isset($auth['VAPID'])) {
+ $auth['VAPID'] = VAPID::validate($auth['VAPID']);
+ }
+
+ $this->auth = $auth;
+
+ $this->setDefaultOptions($defaultOptions);
+
+ if (!array_key_exists('timeout', $clientOptions) && isset($timeout)) {
+ $clientOptions['timeout'] = $timeout;
+ }
+ $this->client = new Client($clientOptions);
+ }
+
+ /**
+ * Send a notification.
+ *
+ * @param SubscriptionInterface $subscription
+ * @param string|null $payload If you want to send an array, json_encode it
+ * @param bool $flush If you want to flush directly (usually when you send only one notification)
+ * @param array $options Array with several options tied to this notification. If not set, will use the default options that you can set in the WebPush object
+ * @param array $auth Use this auth details instead of what you provided when creating WebPush
+ *
+ * @return \Generator|MessageSentReport[]|true Return an array of information if $flush is set to true and the queued requests has failed.
+ * Else return true
+ *
+ * @throws \ErrorException
+ */
+ public function sendNotification(SubscriptionInterface $subscription, ?string $payload = null, bool $flush = false, array $options = [], array $auth = [])
+ {
+ if (isset($payload)) {
+ if (Utils::safeStrlen($payload) > Encryption::MAX_PAYLOAD_LENGTH) {
+ throw new \ErrorException('Size of payload must not be greater than '.Encryption::MAX_PAYLOAD_LENGTH.' octets.');
+ }
+
+ $contentEncoding = $subscription->getContentEncoding();
+ if (!$contentEncoding) {
+ throw new \ErrorException('Subscription should have a content encoding');
+ }
+
+ $payload = Encryption::padPayload($payload, $this->automaticPadding, $contentEncoding);
+ }
+
+ if (array_key_exists('VAPID', $auth)) {
+ $auth['VAPID'] = VAPID::validate($auth['VAPID']);
+ }
+
+ $this->notifications[] = new Notification($subscription, $payload, $options, $auth);
+
+ return false !== $flush ? $this->flush() : true;
+ }
+
+ /**
+ * Flush notifications. Triggers the requests.
+ *
+ * @param null|int $batchSize Defaults the value defined in defaultOptions during instantiation (which defaults to 1000).
+ *
+ * @return \Generator|MessageSentReport[]
+ * @throws \ErrorException
+ */
+ public function flush(?int $batchSize = null): \Generator
+ {
+ if (null === $this->notifications || empty($this->notifications)) {
+ yield from [];
+ return;
+ }
+
+ if (null === $batchSize) {
+ $batchSize = $this->defaultOptions['batchSize'];
+ }
+
+ $batches = array_chunk($this->notifications, $batchSize);
+
+ // reset queue
+ $this->notifications = [];
+
+ foreach ($batches as $batch) {
+ // for each endpoint server type
+ $requests = $this->prepare($batch);
+
+ $promises = [];
+
+ foreach ($requests as $request) {
+ $promises[] = $this->client->sendAsync($request)
+ ->then(function ($response) use ($request) {
+ /** @var ResponseInterface $response * */
+ return new MessageSentReport($request, $response);
+ })
+ ->otherwise(function ($reason) {
+ /** @var RequestException $reason **/
+ return new MessageSentReport($reason->getRequest(), $reason->getResponse(), false, $reason->getMessage());
+ });
+ }
+
+ foreach ($promises as $promise) {
+ yield $promise->wait();
+ }
+ }
+
+ if ($this->reuseVAPIDHeaders) {
+ $this->vapidHeaders = [];
+ }
+ }
+
+ /**
+ * @param array $notifications
+ *
+ * @return array
+ *
+ * @throws \ErrorException
+ */
+ private function prepare(array $notifications): array
+ {
+ $requests = [];
+ /** @var Notification $notification */
+ foreach ($notifications as $notification) {
+ $subscription = $notification->getSubscription();
+ $endpoint = $subscription->getEndpoint();
+ $userPublicKey = $subscription->getPublicKey();
+ $userAuthToken = $subscription->getAuthToken();
+ $contentEncoding = $subscription->getContentEncoding();
+ $payload = $notification->getPayload();
+ $options = $notification->getOptions($this->getDefaultOptions());
+ $auth = $notification->getAuth($this->auth);
+
+ if (!empty($payload) && !empty($userPublicKey) && !empty($userAuthToken)) {
+ if (!$contentEncoding) {
+ throw new \ErrorException('Subscription should have a content encoding');
+ }
+
+ $encrypted = Encryption::encrypt($payload, $userPublicKey, $userAuthToken, $contentEncoding);
+ $cipherText = $encrypted['cipherText'];
+ $salt = $encrypted['salt'];
+ $localPublicKey = $encrypted['localPublicKey'];
+
+ $headers = [
+ 'Content-Type' => 'application/octet-stream',
+ 'Content-Encoding' => $contentEncoding,
+ ];
+
+ if ($contentEncoding === "aesgcm") {
+ $headers['Encryption'] = 'salt='.Base64Url::encode($salt);
+ $headers['Crypto-Key'] = 'dh='.Base64Url::encode($localPublicKey);
+ }
+
+ $encryptionContentCodingHeader = Encryption::getContentCodingHeader($salt, $localPublicKey, $contentEncoding);
+ $content = $encryptionContentCodingHeader.$cipherText;
+
+ $headers['Content-Length'] = Utils::safeStrlen($content);
+ } else {
+ $headers = [
+ 'Content-Length' => 0,
+ ];
+
+ $content = '';
+ }
+
+ $headers['TTL'] = $options['TTL'];
+
+ if (isset($options['urgency'])) {
+ $headers['Urgency'] = $options['urgency'];
+ }
+
+ if (isset($options['topic'])) {
+ $headers['Topic'] = $options['topic'];
+ }
+
+ // if GCM
+ if (substr($endpoint, 0, strlen(self::GCM_URL)) === self::GCM_URL) {
+ if (array_key_exists('GCM', $auth)) {
+ $headers['Authorization'] = 'key='.$auth['GCM'];
+ } else {
+ throw new \ErrorException('No GCM API Key specified.');
+ }
+ }
+ // if VAPID (GCM doesn't support it but FCM does)
+ elseif (array_key_exists('VAPID', $auth) && $contentEncoding) {
+ $audience = parse_url($endpoint, PHP_URL_SCHEME).'://'.parse_url($endpoint, PHP_URL_HOST);
+ if (!parse_url($audience)) {
+ throw new \ErrorException('Audience "'.$audience.'"" could not be generated.');
+ }
+
+ $vapidHeaders = $this->getVAPIDHeaders($audience, $contentEncoding, $auth['VAPID']);
+
+ $headers['Authorization'] = $vapidHeaders['Authorization'];
+
+ if ($contentEncoding === 'aesgcm') {
+ if (array_key_exists('Crypto-Key', $headers)) {
+ $headers['Crypto-Key'] .= ';'.$vapidHeaders['Crypto-Key'];
+ } else {
+ $headers['Crypto-Key'] = $vapidHeaders['Crypto-Key'];
+ }
+ }
+ }
+
+ $requests[] = new Request('POST', $endpoint, $headers, $content);
+ }
+
+ return $requests;
+ }
+
+ /**
+ * @return bool
+ */
+ public function isAutomaticPadding(): bool
+ {
+ return $this->automaticPadding !== 0;
+ }
+
+ /**
+ * @return int
+ */
+ public function getAutomaticPadding()
+ {
+ return $this->automaticPadding;
+ }
+
+ /**
+ * @param int|bool $automaticPadding Max padding length
+ *
+ * @return WebPush
+ *
+ * @throws \Exception
+ */
+ public function setAutomaticPadding($automaticPadding): WebPush
+ {
+ if ($automaticPadding > Encryption::MAX_PAYLOAD_LENGTH) {
+ throw new \Exception('Automatic padding is too large. Max is '.Encryption::MAX_PAYLOAD_LENGTH.'. Recommended max is '.Encryption::MAX_COMPATIBILITY_PAYLOAD_LENGTH.' for compatibility reasons (see README).');
+ } elseif ($automaticPadding < 0) {
+ throw new \Exception('Padding length should be positive or zero.');
+ } elseif ($automaticPadding === true) {
+ $this->automaticPadding = Encryption::MAX_COMPATIBILITY_PAYLOAD_LENGTH;
+ } elseif ($automaticPadding === false) {
+ $this->automaticPadding = 0;
+ } else {
+ $this->automaticPadding = $automaticPadding;
+ }
+
+ return $this;
+ }
+
+ /**
+ * @return bool
+ */
+ public function getReuseVAPIDHeaders()
+ {
+ return $this->reuseVAPIDHeaders;
+ }
+
+ /**
+ * Reuse VAPID headers in the same flush session to improve performance
+ * @param bool $enabled
+ *
+ * @return WebPush
+ */
+ public function setReuseVAPIDHeaders(bool $enabled)
+ {
+ $this->reuseVAPIDHeaders = $enabled;
+
+ return $this;
+ }
+
+ /**
+ * @return array
+ */
+ public function getDefaultOptions(): array
+ {
+ return $this->defaultOptions;
+ }
+
+ /**
+ * @param array $defaultOptions Keys 'TTL' (Time To Live, defaults 4 weeks), 'urgency', 'topic', 'batchSize'
+ *
+ * @return WebPush
+ */
+ public function setDefaultOptions(array $defaultOptions)
+ {
+ $this->defaultOptions['TTL'] = isset($defaultOptions['TTL']) ? $defaultOptions['TTL'] : 2419200;
+ $this->defaultOptions['urgency'] = isset($defaultOptions['urgency']) ? $defaultOptions['urgency'] : null;
+ $this->defaultOptions['topic'] = isset($defaultOptions['topic']) ? $defaultOptions['topic'] : null;
+ $this->defaultOptions['batchSize'] = isset($defaultOptions['batchSize']) ? $defaultOptions['batchSize'] : 1000;
+
+ return $this;
+ }
+
+ /**
+ * @return int
+ */
+ public function countPendingNotifications(): int
+ {
+ return null !== $this->notifications ? count($this->notifications) : 0;
+ }
+
+ /**
+ * @param string $audience
+ * @param string $contentEncoding
+ * @param array $vapid
+ * @return array
+ * @throws \ErrorException
+ */
+ private function getVAPIDHeaders(string $audience, string $contentEncoding, array $vapid)
+ {
+ $vapidHeaders = null;
+
+ $cache_key = null;
+ if ($this->reuseVAPIDHeaders) {
+ $cache_key = implode('#', [$audience, $contentEncoding, crc32(serialize($vapid))]);
+ if (array_key_exists($cache_key, $this->vapidHeaders)) {
+ $vapidHeaders = $this->vapidHeaders[$cache_key];
+ }
+ }
+
+ if (!$vapidHeaders) {
+ $vapidHeaders = VAPID::getVapidHeaders($audience, $vapid['subject'], $vapid['publicKey'], $vapid['privateKey'], $contentEncoding);
+ }
+
+ if ($this->reuseVAPIDHeaders) {
+ $this->vapidHeaders[$cache_key] = $vapidHeaders;
+ }
+
+ return $vapidHeaders;
+ }
+}