Update `minishlink/web-push` to `v9.0.0-rc2`
authorCyperghost <olaf_schmitz_1@t-online.de>
Mon, 24 Jun 2024 08:30:47 +0000 (10:30 +0200)
committerCyperghost <olaf_schmitz_1@t-online.de>
Mon, 24 Jun 2024 08:30:47 +0000 (10:30 +0200)
15 files changed:
wcfsetup/install/files/lib/system/api/composer.json
wcfsetup/install/files/lib/system/api/composer.lock
wcfsetup/install/files/lib/system/api/composer/autoload_files.php
wcfsetup/install/files/lib/system/api/composer/autoload_static.php
wcfsetup/install/files/lib/system/api/composer/installed.json
wcfsetup/install/files/lib/system/api/composer/installed.php
wcfsetup/install/files/lib/system/api/minishlink/web-push/composer.json
wcfsetup/install/files/lib/system/api/minishlink/web-push/phpstan.neon [deleted file]
wcfsetup/install/files/lib/system/api/minishlink/web-push/src/Encryption.php
wcfsetup/install/files/lib/system/api/minishlink/web-push/src/MessageSentReport.php
wcfsetup/install/files/lib/system/api/minishlink/web-push/src/Notification.php
wcfsetup/install/files/lib/system/api/minishlink/web-push/src/Subscription.php
wcfsetup/install/files/lib/system/api/minishlink/web-push/src/Utils.php
wcfsetup/install/files/lib/system/api/minishlink/web-push/src/VAPID.php
wcfsetup/install/files/lib/system/api/minishlink/web-push/src/WebPush.php

index 4d60e6920645cab359d5efbd18daaa8f48709644..f6a591767325b0550fb5ec741fa281d147629379 100644 (file)
@@ -19,7 +19,7 @@
         "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",
index 05cb04fa03a987679abe9ff1cc81dabdc9e97db8..5be39f173703c7d87b28028eb8b301b89579d1f8 100644 (file)
@@ -4,7 +4,7 @@
         "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,
index 3ee8ebc19006b978b04846c136ab658117540262..17123805051addd74838f7d23029b297b00929ff 100644 (file)
@@ -7,9 +7,9 @@ $baseDir = $vendorDir;
 
 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',
index 3e3e5de976aaf80cab1d3b30cb38adf5158d8d46..25590b047b6b4b507733edc3c102a83d32d493c7 100644 (file)
@@ -8,9 +8,9 @@ class ComposerStaticInita1f5f7c74275d47a45049a2936db1d0d
 {
     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',
index 891cfe36de0f3139ba784e05088205deee255f81..7d4948859aec6f499e8fabc942418c614284d296 100644 (file)
         },
         {
             "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"
         },
index ef500128a8003f49a54ddc246c36247235463cb2..ebe9ddb09e1a67550fb58ede2a6876810d7f4d3a 100644 (file)
@@ -3,7 +3,7 @@
         'name' => '__root__',
         'pretty_version' => 'dev-master',
         'version' => 'dev-master',
-        'reference' => '6f6461b4a24316d70856362b6136992572e05b32',
+        'reference' => 'dafe4e4d98ef3d45aa1c838703676812016db738',
         'type' => 'project',
         'install_path' => __DIR__ . '/../',
         'aliases' => array(),
@@ -13,7 +13,7 @@
         '__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(),
index 2b164ac26eaed88506c4af568c2a2f2aa43b8ce1..b9b34b97d4a4a925cba78100b93bd3d32b0aa924 100644 (file)
@@ -2,7 +2,13 @@
   "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
diff --git a/wcfsetup/install/files/lib/system/api/minishlink/web-push/phpstan.neon b/wcfsetup/install/files/lib/system/api/minishlink/web-push/phpstan.neon
deleted file mode 100644 (file)
index f031d80..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-parameters:
-    level: 7
-    paths:
-        - src
-    checkMissingIterableValueType: false
-    reportUnmatchedIgnoredErrors: false
-    ignoreErrors:
-        - '#Unreachable statement \- code above always terminates\.#'
index 404c0336ddd10a018fdb3255e575898807671d50..89fc8e88573bf7fcc51d54a034e3baad7ba53b62 100644 (file)
@@ -14,16 +14,14 @@ declare(strict_types=1);
 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)
@@ -36,11 +34,12 @@ class Encryption
 
         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");
     }
 
     /**
@@ -63,7 +62,7 @@ class Encryption
     }
 
     /**
-     * @throws \ErrorException
+     * @throws \RuntimeException
      */
     public static function deterministicEncrypt(string $payload, string $userPublicKey, string $userAuthToken, string $contentEncoding, array $localKeyObject, string $salt): array
     {
@@ -88,7 +87,7 @@ class Encryption
             ]);
         }
         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
@@ -225,7 +224,9 @@ class Encryption
             }
 
             return 'Content-Encoding: '.$type.chr(0).'P-256'.$context;
-        } elseif ($contentEncoding === "aes128gcm") {
+        }
+
+        if ($contentEncoding === "aes128gcm") {
             return 'Content-Encoding: '.$type.chr(0);
         }
 
@@ -233,61 +234,18 @@ class Encryption
     }
 
     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 [
@@ -297,94 +255,39 @@ class Encryption
                 '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
index c1b04c38a2a02375cccfe2c2af7b277c809b34f7..59ad5fd1c3cf63faf72784d5b21d2d2e2c3d0073 100644 (file)
@@ -1,4 +1,4 @@
-<?php
+<?php declare(strict_types=1);
 /**
  * @author Igor Timoshenkov [it@campoint.net]
  * @started: 03.09.2018 9:21
@@ -14,35 +14,12 @@ use Psr\Http\Message\ResponseInterface;
  */
 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
@@ -110,11 +87,7 @@ class MessageSentReport implements \JsonSerializable
 
     public function getResponseContent(): ?string
     {
-        if (!$this->response) {
-            return null;
-        }
-
-        return $this->response->getBody()->getContents();
+        return $this->response?->getBody()->getContents();
     }
 
     public function jsonSerialize(): array
index d9414d4adf1b0635c2123913ae7299c44eb6ea77..a924fbb3c89347b91917899135135b6f8e8fe77a 100644 (file)
@@ -15,24 +15,16 @@ 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 : 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
index 1f3a022ad85e54dfea64d6c0517fb98507380af7..bad30612d5197d8770f784c68c0f59e55b7c12c4 100644 (file)
@@ -15,38 +15,21 @@ 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;
-
     /**
      * @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";
         }
     }
index a23200aa145accfb515db0b87e43a8bdb7b38894..887acb0abb16bfed96c13d4a56bdc616432d2837 100644 (file)
@@ -14,7 +14,6 @@ declare(strict_types=1);
 namespace Minishlink\WebPush;
 
 use Base64Url\Base64Url;
-use Brick\Math\BigInteger;
 use Jose\Component\Core\JWK;
 use Jose\Component\Core\Util\Ecc\PublicKey;
 
@@ -29,13 +28,8 @@ class Utils
     {
         $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;
     }
@@ -63,4 +57,74 @@ class Utils
             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);
+            }
+        }
+    }
 }
index edfaa23d8d316eee505228ccddef649d386e93ae..5a40cd999a28a84087fd4c61fee605da84048d7b 100644 (file)
@@ -97,7 +97,7 @@ class VAPID
      * @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) {
@@ -176,7 +176,7 @@ class VAPID
 
         return [
             'publicKey'  => Base64Url::encode($binaryPublicKey),
-            'privateKey' => Base64Url::encode($binaryPrivateKey)
+            'privateKey' => Base64Url::encode($binaryPrivateKey),
         ];
     }
 }
index 97784f1bbcfca7cd3fba02c9992c8389823d6f74..8739a98d5ea94b75350d10c0fce851160a0cd0a6 100644 (file)
@@ -15,69 +15,53 @@ namespace Minishlink\WebPush;
 
 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']);
@@ -140,7 +124,7 @@ class WebPush
      *
      * @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
@@ -193,7 +177,59 @@ class WebPush
     }
 
     /**
-     * @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
     {
@@ -281,50 +317,45 @@ class WebPush
         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;
 
@@ -337,16 +368,16 @@ class WebPush
     }
 
     /**
-     * @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;
     }
@@ -357,10 +388,9 @@ class WebPush
     }
 
     /**
-     * @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;