"laminas/laminas-diactoros": "^3.3.0",
"laminas/laminas-httphandlerrunner": "^2.10.0",
"laminas/laminas-progressbar": "^2.13",
- "minishlink/web-push": "^8.0",
+ "minishlink/web-push": "^v9.0.0-rc2",
"nikic/fast-route": "2.0.0-beta1",
"paragonie/constant_time_encoding": "^2.6.3",
"pelago/emogrifier": "^7.2.0",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "f2c08143de462de937858295f4bd8660",
+ "content-hash": "57415f3e44f525b3bdcdb2e419ea09e4",
"packages": [
{
"name": "brick/math",
},
{
"name": "minishlink/web-push",
- "version": "v8.0.0",
+ "version": "v9.0.0-rc2",
"source": {
"type": "git",
"url": "https://github.com/web-push-libs/web-push-php.git",
- "reference": "ec034f1e287cd1e74235e349bd017d71a61e9d8d"
+ "reference": "9d36211c435baecded11d7a227f5caa098f52f80"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/web-push-libs/web-push-php/zipball/ec034f1e287cd1e74235e349bd017d71a61e9d8d",
- "reference": "ec034f1e287cd1e74235e349bd017d71a61e9d8d",
+ "url": "https://api.github.com/repos/web-push-libs/web-push-php/zipball/9d36211c435baecded11d7a227f5caa098f52f80",
+ "reference": "9d36211c435baecded11d7a227f5caa098f52f80",
"shasum": ""
},
"require": {
"ext-json": "*",
"ext-mbstring": "*",
"ext-openssl": "*",
- "guzzlehttp/guzzle": "^7.0.1|^6.2",
- "php": ">=8.0",
- "spomky-labs/base64url": "^2.0",
- "web-token/jwt-key-mgmt": "^2.0|^3.0.2",
- "web-token/jwt-signature": "^2.0|^3.0.2",
- "web-token/jwt-signature-algorithm-ecdsa": "^2.0|^3.0.2",
- "web-token/jwt-util-ecc": "^2.0|^3.0.2"
+ "guzzlehttp/guzzle": "^7.4.5",
+ "php": ">=8.1",
+ "spomky-labs/base64url": "^2.0.4",
+ "web-token/jwt-library": "^3.3.0"
},
"require-dev": {
- "friendsofphp/php-cs-fixer": "^v3.13.2",
- "phpstan/phpstan": "^1.9.8",
- "phpunit/phpunit": "^9.5.27"
+ "friendsofphp/php-cs-fixer": "^v3.48.0",
+ "phpstan/phpstan": "^1.10.57",
+ "phpunit/phpunit": "^10.5.9"
},
"suggest": {
+ "ext-bcmath": "Optional for performance.",
"ext-gmp": "Optional for performance."
},
"type": "library",
],
"support": {
"issues": "https://github.com/web-push-libs/web-push-php/issues",
- "source": "https://github.com/web-push-libs/web-push-php/tree/v8.0.0"
+ "source": "https://github.com/web-push-libs/web-push-php/tree/v9.0.0-rc2"
},
- "time": "2023-01-10T17:14:44+00:00"
+ "time": "2024-06-18T16:26:43+00:00"
},
{
"name": "nikic/fast-route",
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {
+ "minishlink/web-push": 5,
"nikic/fast-route": 10
},
"prefer-stable": false,
return array(
'7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php',
+ '3109cb1a231dcd04bee1f9f620d46975' => $vendorDir . '/paragonie/sodium_compat/autoload.php',
'6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',
- '3109cb1a231dcd04bee1f9f620d46975' => $vendorDir . '/paragonie/sodium_compat/autoload.php',
'2cffec82183ee1cea088009cef9a6fc3' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier.composer.php',
'07d7f1a47144818725fd8d91a907ac57' => $vendorDir . '/laminas/laminas-diactoros/src/functions/create_uploaded_file.php',
'da94ac5d3ca7d2dbab84ce561ce72bfd' => $vendorDir . '/laminas/laminas-diactoros/src/functions/marshal_headers_from_sapi.php',
{
public static $files = array (
'7b11c4dc42b3b3023073cb14e519683c' => __DIR__ . '/..' . '/ralouphie/getallheaders/src/getallheaders.php',
+ '3109cb1a231dcd04bee1f9f620d46975' => __DIR__ . '/..' . '/paragonie/sodium_compat/autoload.php',
'6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php',
- '3109cb1a231dcd04bee1f9f620d46975' => __DIR__ . '/..' . '/paragonie/sodium_compat/autoload.php',
'2cffec82183ee1cea088009cef9a6fc3' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier.composer.php',
'07d7f1a47144818725fd8d91a907ac57' => __DIR__ . '/..' . '/laminas/laminas-diactoros/src/functions/create_uploaded_file.php',
'da94ac5d3ca7d2dbab84ce561ce72bfd' => __DIR__ . '/..' . '/laminas/laminas-diactoros/src/functions/marshal_headers_from_sapi.php',
},
{
"name": "minishlink/web-push",
- "version": "v8.0.0",
- "version_normalized": "8.0.0.0",
+ "version": "v9.0.0-rc2",
+ "version_normalized": "9.0.0.0-RC2",
"source": {
"type": "git",
"url": "https://github.com/web-push-libs/web-push-php.git",
- "reference": "ec034f1e287cd1e74235e349bd017d71a61e9d8d"
+ "reference": "9d36211c435baecded11d7a227f5caa098f52f80"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/web-push-libs/web-push-php/zipball/ec034f1e287cd1e74235e349bd017d71a61e9d8d",
- "reference": "ec034f1e287cd1e74235e349bd017d71a61e9d8d",
+ "url": "https://api.github.com/repos/web-push-libs/web-push-php/zipball/9d36211c435baecded11d7a227f5caa098f52f80",
+ "reference": "9d36211c435baecded11d7a227f5caa098f52f80",
"shasum": ""
},
"require": {
"ext-json": "*",
"ext-mbstring": "*",
"ext-openssl": "*",
- "guzzlehttp/guzzle": "^7.0.1|^6.2",
- "php": ">=8.0",
- "spomky-labs/base64url": "^2.0",
- "web-token/jwt-key-mgmt": "^2.0|^3.0.2",
- "web-token/jwt-signature": "^2.0|^3.0.2",
- "web-token/jwt-signature-algorithm-ecdsa": "^2.0|^3.0.2",
- "web-token/jwt-util-ecc": "^2.0|^3.0.2"
+ "guzzlehttp/guzzle": "^7.4.5",
+ "php": ">=8.1",
+ "spomky-labs/base64url": "^2.0.4",
+ "web-token/jwt-library": "^3.3.0"
},
"require-dev": {
- "friendsofphp/php-cs-fixer": "^v3.13.2",
- "phpstan/phpstan": "^1.9.8",
- "phpunit/phpunit": "^9.5.27"
+ "friendsofphp/php-cs-fixer": "^v3.48.0",
+ "phpstan/phpstan": "^1.10.57",
+ "phpunit/phpunit": "^10.5.9"
},
"suggest": {
+ "ext-bcmath": "Optional for performance.",
"ext-gmp": "Optional for performance."
},
- "time": "2023-01-10T17:14:44+00:00",
+ "time": "2024-06-18T16:26:43+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
],
"support": {
"issues": "https://github.com/web-push-libs/web-push-php/issues",
- "source": "https://github.com/web-push-libs/web-push-php/tree/v8.0.0"
+ "source": "https://github.com/web-push-libs/web-push-php/tree/v9.0.0-rc2"
},
"install-path": "../minishlink/web-push"
},
'name' => '__root__',
'pretty_version' => 'dev-master',
'version' => 'dev-master',
- 'reference' => '6f6461b4a24316d70856362b6136992572e05b32',
+ 'reference' => 'dafe4e4d98ef3d45aa1c838703676812016db738',
'type' => 'project',
'install_path' => __DIR__ . '/../',
'aliases' => array(),
'__root__' => array(
'pretty_version' => 'dev-master',
'version' => 'dev-master',
- 'reference' => '6f6461b4a24316d70856362b6136992572e05b32',
+ 'reference' => 'dafe4e4d98ef3d45aa1c838703676812016db738',
'type' => 'project',
'install_path' => __DIR__ . '/../',
'aliases' => array(),
'dev_requirement' => false,
),
'minishlink/web-push' => array(
- 'pretty_version' => 'v8.0.0',
- 'version' => '8.0.0.0',
- 'reference' => 'ec034f1e287cd1e74235e349bd017d71a61e9d8d',
+ 'pretty_version' => 'v9.0.0-rc2',
+ 'version' => '9.0.0.0-RC2',
+ 'reference' => '9d36211c435baecded11d7a227f5caa098f52f80',
'type' => 'library',
'install_path' => __DIR__ . '/../minishlink/web-push',
'aliases' => array(),
"name": "minishlink/web-push",
"type": "library",
"description": "Web Push library for PHP",
- "keywords": ["push", "notifications", "web", "WebPush", "Push API"],
+ "keywords": [
+ "push",
+ "notifications",
+ "web",
+ "WebPush",
+ "Push API"
+ ],
"homepage": "https://github.com/web-push-libs/web-push-php",
"license": "MIT",
"authors": [
}
],
"scripts": {
- "test:unit": "./vendor/bin/phpunit --color",
+ "fix:syntax": "./vendor/bin/php-cs-fixer fix ./src --using-cache=no",
+ "fix:syntax_tests": "./vendor/bin/php-cs-fixer fix ./tests --using-cache=no",
+ "test:unit": "./vendor/bin/phpunit",
+ "test:unit_offline": "./vendor/bin/phpunit --exclude-group=online",
"test:typing": "./vendor/bin/phpstan analyse",
- "test:syntax": "./vendor/bin/php-cs-fixer fix ./src --dry-run --stop-on-violation --using-cache=no"
+ "test:syntax": "./vendor/bin/php-cs-fixer fix ./src --dry-run --stop-on-violation --using-cache=no",
+ "test:syntax_tests": "./vendor/bin/php-cs-fixer fix ./tests --dry-run --stop-on-violation --using-cache=no"
},
"require": {
- "php": ">=8.0",
+ "php": ">=8.1",
"ext-curl": "*",
"ext-json": "*",
"ext-mbstring": "*",
"ext-openssl": "*",
- "guzzlehttp/guzzle": "^7.0.1|^6.2",
- "web-token/jwt-signature": "^2.0|^3.0.2",
- "web-token/jwt-key-mgmt": "^2.0|^3.0.2",
- "web-token/jwt-signature-algorithm-ecdsa": "^2.0|^3.0.2",
- "web-token/jwt-util-ecc": "^2.0|^3.0.2",
- "spomky-labs/base64url": "^2.0"
+ "guzzlehttp/guzzle": "^7.4.5",
+ "web-token/jwt-library": "^3.3.0",
+ "spomky-labs/base64url": "^2.0.4"
},
"suggest": {
+ "ext-bcmath": "Optional for performance.",
"ext-gmp": "Optional for performance."
},
"require-dev": {
- "phpunit/phpunit": "^9.5.27",
- "phpstan/phpstan": "^1.9.8",
- "friendsofphp/php-cs-fixer": "^v3.13.2"
+ "phpunit/phpunit": "^10.5.9",
+ "phpstan/phpstan": "^1.10.57",
+ "friendsofphp/php-cs-fixer": "^v3.48.0"
},
"autoload": {
- "psr-4" : {
- "Minishlink\\WebPush\\" : "src"
+ "psr-4": {
+ "Minishlink\\WebPush\\": "src"
}
}
-}
+}
\ No newline at end of file
+++ /dev/null
-parameters:
- level: 7
- paths:
- - src
- checkMissingIterableValueType: false
- reportUnmatchedIgnoredErrors: false
- ignoreErrors:
- - '#Unreachable statement \- code above always terminates\.#'
namespace Minishlink\WebPush;
use Base64Url\Base64Url;
-use Brick\Math\BigInteger;
use Jose\Component\Core\JWK;
-use Jose\Component\Core\Util\Ecc\NistCurve;
use Jose\Component\Core\Util\Ecc\PrivateKey;
use Jose\Component\Core\Util\ECKey;
class Encryption
{
public const MAX_PAYLOAD_LENGTH = 4078;
- public const MAX_COMPATIBILITY_PAYLOAD_LENGTH = 3052;
+ public const MAX_COMPATIBILITY_PAYLOAD_LENGTH = 2820;
/**
* @return string padded payload (plaintext)
if ($contentEncoding === "aesgcm") {
return pack('n*', $padLen).str_pad($payload, $padLen + $payloadLen, chr(0), STR_PAD_LEFT);
- } elseif ($contentEncoding === "aes128gcm") {
+ }
+ if ($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");
}
+
+ throw new \ErrorException("This content encoding is not supported");
}
/**
}
/**
- * @throws \ErrorException
+ * @throws \RuntimeException
*/
public static function deterministicEncrypt(string $payload, string $userPublicKey, string $userAuthToken, string $contentEncoding, array $localKeyObject, string $salt): array
{
]);
}
if (!$localPublicKey) {
- throw new \ErrorException('Failed to convert local public key from hexadecimal to binary');
+ throw new \RuntimeException('Failed to convert local public key from hexadecimal to binary.');
}
// get user public key object
}
return 'Content-Encoding: '.$type.chr(0).'P-256'.$context;
- } elseif ($contentEncoding === "aes128gcm") {
+ }
+
+ if ($contentEncoding === "aes128gcm") {
return 'Content-Encoding: '.$type.chr(0);
}
}
private static function createLocalKeyObject(): array
- {
- try {
- return self::createLocalKeyObjectUsingOpenSSL();
- } catch (\Exception $e) {
- return self::createLocalKeyObjectUsingPurePhpMethod();
- }
- }
-
- private static function createLocalKeyObjectUsingPurePhpMethod(): array
- {
- $curve = NistCurve::curve256();
- $privateKey = $curve->createPrivateKey();
- $publicKey = $curve->createPublicKey($privateKey);
-
- if ($publicKey->getPoint()->getX() instanceof BigInteger) {
- return [
- new JWK([
- 'kty' => 'EC',
- 'crv' => 'P-256',
- 'x' => Base64Url::encode(self::addNullPadding($publicKey->getPoint()->getX()->toBytes(false))),
- 'y' => Base64Url::encode(self::addNullPadding($publicKey->getPoint()->getY()->toBytes(false))),
- 'd' => Base64Url::encode(self::addNullPadding($privateKey->getSecret()->toBytes(false))),
- ])
- ];
- }
-
- return [
- new JWK([
- 'kty' => 'EC',
- 'crv' => 'P-256',
- 'x' => Base64Url::encode(self::addNullPadding(hex2bin(gmp_strval($publicKey->getPoint()->getX(), 16)))),
- 'y' => Base64Url::encode(self::addNullPadding(hex2bin(gmp_strval($publicKey->getPoint()->getY(), 16)))),
- 'd' => Base64Url::encode(self::addNullPadding(hex2bin(gmp_strval($privateKey->getSecret(), 16)))),
- ])
- ];
- }
-
- 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');
+ throw new \RuntimeException('Unable to create the local key.');
}
$details = openssl_pkey_get_details($keyResource);
- if (PHP_MAJOR_VERSION < 8) {
- openssl_pkey_free($keyResource);
- }
-
if (!$details) {
- throw new \RuntimeException('Unable to get the key details');
+ throw new \RuntimeException('Unable to get the local key details.');
}
return [
'x' => Base64Url::encode(self::addNullPadding($details['ec']['x'])),
'y' => Base64Url::encode(self::addNullPadding($details['ec']['y'])),
'd' => Base64Url::encode(self::addNullPadding($details['ec']['d'])),
- ])
+ ]),
];
}
/**
- * @throws \ErrorException
+ * @throws \ValueError
*/
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;
- }
-
- private static function calculateAgreementKey(JWK $private_key, JWK $public_key): string
- {
- if (function_exists('openssl_pkey_derive')) {
- try {
- $publicPem = ECKey::convertPublicKeyToPEM($public_key);
- $privatePem = ECKey::convertPrivateKeyToPEM($private_key);
-
- $result = openssl_pkey_derive($publicPem, $privatePem, 256); // @phpstan-ignore-line
- if ($result === false) {
- throw new \Exception('Unable to compute the agreement key');
- }
- return $result;
- } catch (\Throwable $throwable) {
- //Does nothing. Will fallback to the pure PHP function
- }
- }
-
-
- $curve = NistCurve::curve256();
- try {
- $rec_x = self::convertBase64ToBigInteger($public_key->get('x'));
- $rec_y = self::convertBase64ToBigInteger($public_key->get('y'));
- $sen_d = self::convertBase64ToBigInteger($private_key->get('d'));
- $priv_key = PrivateKey::create($sen_d);
- $pub_key = $curve->getPublicKeyFrom($rec_x, $rec_y);
-
- return hex2bin(str_pad($curve->mul($pub_key->getPoint(), $priv_key->getSecret())->getX()->toBase(16), 64, '0', STR_PAD_LEFT)); // @phpstan-ignore-line
- } catch (\Throwable $e) {
- $rec_x = self::convertBase64ToGMP($public_key->get('x'));
- $rec_y = self::convertBase64ToGMP($public_key->get('y'));
- $sen_d = self::convertBase64ToGMP($private_key->get('d'));
- $priv_key = PrivateKey::create($sen_d); // @phpstan-ignore-line
- $pub_key = $curve->getPublicKeyFrom($rec_x, $rec_y); // @phpstan-ignore-line
-
- return hex2bin(gmp_strval($curve->mul($pub_key->getPoint(), $priv_key->getSecret())->getX(), 16)); // @phpstan-ignore-line
+ if (empty($userAuthToken)) {
+ return $sharedSecret;
}
- }
-
- /**
- * @throws \ErrorException
- */
- private static function convertBase64ToBigInteger(string $value): BigInteger
- {
- $value = unpack('H*', Base64Url::decode($value));
-
- if ($value === false) {
- throw new \ErrorException('Unable to unpack hex value from string');
+ if($contentEncoding === "aesgcm") {
+ $info = 'Content-Encoding: auth'.chr(0);
+ } elseif($contentEncoding === "aes128gcm") {
+ $info = "WebPush: info".chr(0).$userPublicKey.$localPublicKey;
+ } else {
+ throw new \ValueError("This content encoding is not supported.");
}
- return BigInteger::fromBase($value[1], 16);
+ return self::hkdf($userAuthToken, $sharedSecret, $info, 32);
}
- /**
- * @throws \ErrorException
- */
- private static function convertBase64ToGMP(string $value): \GMP
+ private static function calculateAgreementKey(JWK $private_key, JWK $public_key): string
{
- $value = unpack('H*', Base64Url::decode($value));
+ $publicPem = ECKey::convertPublicKeyToPEM($public_key);
+ $privatePem = ECKey::convertPrivateKeyToPEM($private_key);
- if ($value === false) {
- throw new \ErrorException('Unable to unpack hex value from string');
+ $result = openssl_pkey_derive($publicPem, $privatePem, 256);
+ if ($result === false) {
+ throw new \RuntimeException('Unable to compute the agreement key.');
}
-
- return gmp_init($value[1], 16);
+ return $result;
}
private static function addNullPadding(string $data): string
-<?php
+<?php declare(strict_types=1);
/**
* @author Igor Timoshenkov [it@campoint.net]
* @started: 03.09.2018 9:21
*/
class MessageSentReport implements \JsonSerializable
{
- /**
- * @var boolean
- */
- protected $success;
-
- /**
- * @var RequestInterface
- */
- protected $request;
-
- /**
- * @var ResponseInterface | null
- */
- protected $response;
-
- /**
- * @var string
- */
- protected $reason;
-
- /**
- * @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;
+ public function __construct(
+ protected RequestInterface $request,
+ protected ?ResponseInterface $response = null,
+ protected bool $success = true,
+ protected string $reason = 'OK'
+ ) {
}
public function isSuccess(): bool
public function getResponseContent(): ?string
{
- if (!$this->response) {
- return null;
- }
-
- return $this->response->getBody()->getContents();
+ return $this->response?->getBody()->getContents();
}
public function jsonSerialize(): array
class Notification
{
- /** @var SubscriptionInterface */
- private $subscription;
-
- /** @var null|string */
- private $payload;
-
- /** @var array Options : TTL, urgency, topic */
- private $options;
-
- /** @var array Auth details : VAPID */
- private $auth;
-
- public function __construct(SubscriptionInterface $subscription, ?string $payload, array $options, array $auth)
- {
- $this->subscription = $subscription;
- $this->payload = $payload;
- $this->options = $options;
- $this->auth = $auth;
+ /**
+ * @param array $options Options: TTL, urgency, topic
+ * @param array $auth Auth details: VAPID
+ */
+ public function __construct(
+ private SubscriptionInterface $subscription,
+ private ?string $payload,
+ private array $options,
+ private array $auth
+ ) {
}
public function getSubscription(): SubscriptionInterface
class Subscription implements SubscriptionInterface
{
- /** @var string */
- private $endpoint;
-
- /** @var null|string */
- private $publicKey;
-
- /** @var null|string */
- private $authToken;
-
- /** @var null|string */
- private $contentEncoding;
-
/**
* @param string|null $contentEncoding (Optional) Must be "aesgcm"
* @throws \ErrorException
*/
public function __construct(
- string $endpoint,
- ?string $publicKey = null,
- ?string $authToken = null,
- ?string $contentEncoding = null
+ private string $endpoint,
+ private ?string $publicKey = null,
+ private ?string $authToken = null,
+ private ?string $contentEncoding = null
) {
- $this->endpoint = $endpoint;
-
- if ($publicKey || $authToken || $contentEncoding) {
+ if($publicKey || $authToken || $contentEncoding) {
$supportedContentEncodings = ['aesgcm', 'aes128gcm'];
- if ($contentEncoding && !in_array($contentEncoding, $supportedContentEncodings)) {
+ if ($contentEncoding && !in_array($contentEncoding, $supportedContentEncodings, true)) {
throw new \ErrorException('This content encoding ('.$contentEncoding.') is not supported.');
}
-
- $this->publicKey = $publicKey;
- $this->authToken = $authToken;
$this->contentEncoding = $contentEncoding ?: "aesgcm";
}
}
namespace Minishlink\WebPush;
use Base64Url\Base64Url;
-use Brick\Math\BigInteger;
use Jose\Component\Core\JWK;
use Jose\Component\Core\Util\Ecc\PublicKey;
{
$hexString = '04';
$point = $publicKey->getPoint();
- if ($point->getX() instanceof BigInteger) {
- $hexString .= str_pad($point->getX()->toBase(16), 64, '0', STR_PAD_LEFT);
- $hexString .= str_pad($point->getY()->toBase(16), 64, '0', STR_PAD_LEFT);
- } else { // @phpstan-ignore-line
- $hexString .= str_pad(gmp_strval($point->getX(), 16), 64, '0', STR_PAD_LEFT);
- $hexString .= str_pad(gmp_strval($point->getY(), 16), 64, '0', STR_PAD_LEFT); // @phpstan-ignore-line
- }
+ $hexString .= str_pad($point->getX()->toBase(16), 64, '0', STR_PAD_LEFT);
+ $hexString .= str_pad($point->getY()->toBase(16), 64, '0', STR_PAD_LEFT);
return $hexString;
}
hex2bin(mb_substr($data, $dataLength / 2, null, '8bit')),
];
}
+
+ /**
+ * Generates user warning/notice if some requirements are not met.
+ * Does not throw exception to allow unusual or polyfill environments.
+ */
+ public static function checkRequirement(): void
+ {
+ self::checkRequirementExtension();
+ self::checkRequirementKeyCipherHash();
+ }
+
+ public static function checkRequirementExtension(): void
+ {
+ $requiredExtensions = [
+ 'curl' => '[WebPush] curl extension is not loaded but is required. 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($requiredExtensions as $extension => $message) {
+ if(!extension_loaded($extension)) {
+ trigger_error($message, E_USER_WARNING);
+ }
+ }
+
+ // Check optional extensions.
+ if(!extension_loaded("bcmath") && !extension_loaded("gmp")) {
+ trigger_error("It is highly recommended to install the GMP or BCMath extension to speed up calculations. The fastest available calculator implementation will be automatically selected at runtime.", E_USER_NOTICE);
+ }
+ }
+
+ public static function checkRequirementKeyCipherHash(): void
+ {
+ // Print your current openssl version with: OPENSSL_VERSION_TEXT
+ // Check for outdated openssl without EC support.
+ $requiredCurves = [
+ 'prime256v1' => '[WebPush] Openssl does not support required curve prime256v1.',
+ ];
+ $availableCurves = openssl_get_curve_names();
+ if($availableCurves === false) {
+ trigger_error('[WebPush] Openssl does not support curves.', E_USER_WARNING);
+ } else {
+ foreach($requiredCurves as $curve => $message) {
+ if(!in_array($curve, $availableCurves, true)) {
+ trigger_error($message, E_USER_WARNING);
+ }
+ }
+ }
+
+ // Check for unusual openssl without cipher support.
+ $requiredCiphers = [
+ 'aes-128-gcm' => '[WebPush] Openssl does not support required cipher aes-128-gcm.',
+ ];
+ $availableCiphers = openssl_get_cipher_methods();
+ foreach($requiredCiphers as $cipher => $message) {
+ if(!in_array($cipher, $availableCiphers, true)) {
+ trigger_error($message, E_USER_WARNING);
+ }
+ }
+
+ // Check for unusual php without hash algo support.
+ $requiredHash = [
+ 'sha256' => '[WebPush] Php does not support required hmac hash sha256.',
+ ];
+ $availableHash = hash_hmac_algos();
+ foreach($requiredHash as $hash => $message) {
+ if(!in_array($hash, $availableHash, true)) {
+ trigger_error($message, E_USER_WARNING);
+ }
+ }
+ }
}
* @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)
+ public static function getVapidHeaders(string $audience, string $subject, string $publicKey, string $privateKey, string $contentEncoding, ?int $expiration = null): array
{
$expirationLimit = time() + 43200; // equal margin of error between 0 and 24h
if (null === $expiration || $expiration > $expirationLimit) {
return [
'publicKey' => Base64Url::encode($binaryPublicKey),
- 'privateKey' => Base64Url::encode($binaryPrivateKey)
+ 'privateKey' => Base64Url::encode($binaryPrivateKey),
];
}
}
use Base64Url\Base64Url;
use GuzzleHttp\Client;
+use GuzzleHttp\Pool;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Psr7\Request;
use Psr\Http\Message\ResponseInterface;
class WebPush
{
- /**
- * @var Client
- */
- protected $client;
-
- /**
- * @var array
- */
- protected $auth;
+ protected Client $client;
+ protected array $auth;
/**
* @var null|array Array of array of Notifications
*/
- protected $notifications;
+ protected ?array $notifications = null;
/**
- * @var array Default options : TTL, urgency, topic, batchSize
+ * @var array Default options: TTL, urgency, topic, batchSize, requestConcurrency
*/
- protected $defaultOptions;
+ protected array $defaultOptions;
/**
* @var int Automatic padding of payloads, if disabled, trade security for bandwidth
*/
- protected $automaticPadding = Encryption::MAX_COMPATIBILITY_PAYLOAD_LENGTH;
+ protected int $automaticPadding = Encryption::MAX_COMPATIBILITY_PAYLOAD_LENGTH;
/**
* @var bool Reuse VAPID headers in the same flush session to improve performance
*/
- protected $reuseVAPIDHeaders = false;
+ protected bool $reuseVAPIDHeaders = false;
/**
* @var array Dictionary for VAPID headers cache
*/
- protected $vapidHeaders = [];
+ protected array $vapidHeaders = [];
/**
* WebPush constructor.
*
- * @param array $auth Some servers needs authentication
- * @param array $defaultOptions TTL, urgency, topic, batchSize
+ * @param array $auth Some servers need authentication
+ * @param array $defaultOptions TTL, urgency, topic, batchSize, requestConcurrency
* @param int|null $timeout Timeout of POST request
*
* @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.',
- '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);
- }
- }
+ Utils::checkRequirement();
if (isset($auth['VAPID'])) {
$auth['VAPID'] = VAPID::validate($auth['VAPID']);
*
* @param null|int $batchSize Defaults the value defined in defaultOptions during instantiation (which defaults to 1000).
*
- * @return \Generator|MessageSentReport[]
+ * @return \Generator
* @throws \ErrorException
*/
public function flush(?int $batchSize = null): \Generator
}
/**
- * @throws \ErrorException
+ * Flush notifications. Triggers concurrent requests.
+ *
+ * @param callable(MessageSentReport): void $callback Callback for each notification
+ * @param null|int $batchSize Defaults the value defined in defaultOptions during instantiation (which defaults to 1000).
+ * @param null|int $requestConcurrency Defaults the value defined in defaultOptions during instantiation (which defaults to 100).
+ */
+ public function flushPooled($callback, ?int $batchSize = null, ?int $requestConcurrency = null): void
+ {
+ if (empty($this->notifications)) {
+ return;
+ }
+
+ if (null === $batchSize) {
+ $batchSize = $this->defaultOptions['batchSize'];
+ }
+
+ if (null === $requestConcurrency) {
+ $requestConcurrency = $this->defaultOptions['requestConcurrency'];
+ }
+
+ $batches = array_chunk($this->notifications, $batchSize);
+ $this->notifications = [];
+
+ foreach ($batches as $batch) {
+ $batch = $this->prepare($batch);
+ $pool = new Pool($this->client, $batch, [
+ 'requestConcurrency' => $requestConcurrency,
+ 'fulfilled' => function (ResponseInterface $response, int $index) use ($callback, $batch) {
+ /** @var \Psr\Http\Message\RequestInterface $request **/
+ $request = $batch[$index];
+ $callback(new MessageSentReport($request, $response));
+ },
+ 'rejected' => function (RequestException $reason) use ($callback) {
+ if (method_exists($reason, 'getResponse')) {
+ $response = $reason->getResponse();
+ } else {
+ $response = null;
+ }
+ $callback(new MessageSentReport($reason->getRequest(), $response, false, $reason->getMessage()));
+ },
+ ]);
+
+ $promise = $pool->promise();
+ $promise->wait();
+ }
+
+ if ($this->reuseVAPIDHeaders) {
+ $this->vapidHeaders = [];
+ }
+ }
+
+ /**
+ * @throws \ErrorException|\Random\RandomException
*/
protected function prepare(array $notifications): array
{
return $this->automaticPadding !== 0;
}
- /**
- * @return int
- */
- public function getAutomaticPadding()
+ public function getAutomaticPadding(): int
{
return $this->automaticPadding;
}
/**
- * @param int|bool $automaticPadding Max padding length
+ * @param bool|int $automaticPadding Max padding length
*
- * @throws \Exception
+ * @throws \ValueError
*/
- public function setAutomaticPadding($automaticPadding): WebPush
+ public function setAutomaticPadding(bool|int $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;
+ if ($automaticPadding === true) {
+ $automaticPadding = Encryption::MAX_COMPATIBILITY_PAYLOAD_LENGTH;
} elseif ($automaticPadding === false) {
- $this->automaticPadding = 0;
- } else {
- $this->automaticPadding = $automaticPadding;
+ $automaticPadding = 0;
+ }
+
+ if($automaticPadding > Encryption::MAX_PAYLOAD_LENGTH) {
+ throw new \ValueError('Automatic padding is too large. Max is '.Encryption::MAX_PAYLOAD_LENGTH.'. Recommended max is '.Encryption::MAX_COMPATIBILITY_PAYLOAD_LENGTH.' for compatibility reasons (see README).');
+ }
+ if($automaticPadding < 0) {
+ throw new \ValueError('Padding length should be positive or zero.');
}
+ $this->automaticPadding = $automaticPadding;
+
return $this;
}
- /**
- * @return bool
- */
- public function getReuseVAPIDHeaders()
+ public function getReuseVAPIDHeaders(): bool
{
return $this->reuseVAPIDHeaders;
}
/**
* Reuse VAPID headers in the same flush session to improve performance
- *
- * @return WebPush
*/
- public function setReuseVAPIDHeaders(bool $enabled)
+ public function setReuseVAPIDHeaders(bool $enabled): WebPush
{
$this->reuseVAPIDHeaders = $enabled;
}
/**
- * @param array $defaultOptions Keys 'TTL' (Time To Live, defaults 4 weeks), 'urgency', 'topic', 'batchSize'
- *
- * @return WebPush
+ * @param array $defaultOptions Keys 'TTL' (Time To Live, defaults 4 weeks), 'urgency', 'topic', 'batchSize', 'requestConcurrency'
*/
- public function setDefaultOptions(array $defaultOptions)
+ public function setDefaultOptions(array $defaultOptions): WebPush
{
$this->defaultOptions['TTL'] = $defaultOptions['TTL'] ?? 2419200;
$this->defaultOptions['urgency'] = $defaultOptions['urgency'] ?? null;
$this->defaultOptions['topic'] = $defaultOptions['topic'] ?? null;
$this->defaultOptions['batchSize'] = $defaultOptions['batchSize'] ?? 1000;
+ $this->defaultOptions['requestConcurrency'] = $defaultOptions['requestConcurrency'] ?? 100;
+
return $this;
}
}
/**
- * @return array
* @throws \ErrorException
*/
- protected function getVAPIDHeaders(string $audience, string $contentEncoding, array $vapid)
+ protected function getVAPIDHeaders(string $audience, string $contentEncoding, array $vapid): ?array
{
$vapidHeaders = null;