ソースを参照

Xero: update bundled Guzzle and psr7 (#5797)

* Xero: update bundled Guzzle, psr7 and the Xero SDK

composer update in modules/boonex/xero, staying on xero-php-oauth2 ^10:
guzzle 7.9.3 -> 7.15.5, psr7 2.7.1 -> 2.13.1, promises 2.2.0 -> 2.5.3,
oauth2-client 2.8.1 -> 2.9.1, xero-php-oauth2 10.0.0 -> 10.4.0.
Fixes the Guzzle and psr7 Dependabot advisories (host-check bypass,
cookie scope, proxy header leaks, CRLF injection). php-jwt stays on 6.x
because SDK 10 requires ^6.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Xero: keep the SDK on 10.0.0

xero-php-oauth2 10.1.0+ adds Contact.TaxNumberType with only the US values
SSN/EIN/ITIN/ATIN, and its setter throws on anything else. Deserialization
calls that setter, so a contact Xero returns with any other value would make
the whole contact lookup fail. 10.0.0 has no such field. Pin it exactly and
keep the Guzzle, psr7 and oauth2-client updates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Sonar: exclude third-party libraries bundled in modules/*/plugins

Updating a module's bundled Composer packages (here Guzzle in Xero) fails the
security-rating gate on findings inside the vendor code. Keep the existing
exclusions and add modules/**/plugins/**.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Andrey Yasko 1 週間 前
親
コミット
10bd5a576a
99 ファイル変更、10816 行追加、1093 行削除
  1. 6 0
      .sonarcloud.properties
  2. 1 1
      modules/boonex/xero/composer.json
  3. 5 0
      modules/boonex/xero/plugins/composer/autoload_classmap.php
  4. 2 1
      modules/boonex/xero/plugins/composer/autoload_files.php
  5. 1 0
      modules/boonex/xero/plugins/composer/autoload_psr4.php
  6. 28 14
      modules/boonex/xero/plugins/composer/autoload_static.php
  7. 146 48
      modules/boonex/xero/plugins/composer/installed.json
  8. 30 21
      modules/boonex/xero/plugins/composer/installed.php
  9. 2 3
      modules/boonex/xero/plugins/composer/platform_check.php
  10. 411 2
      modules/boonex/xero/plugins/guzzlehttp/guzzle/CHANGELOG.md
  11. 7 7
      modules/boonex/xero/plugins/guzzlehttp/guzzle/README.md
  12. 6 8
      modules/boonex/xero/plugins/guzzlehttp/guzzle/UPGRADING.md
  13. 46 44
      modules/boonex/xero/plugins/guzzlehttp/guzzle/composer.json
  14. 813 28
      modules/boonex/xero/plugins/guzzlehttp/guzzle/src/Client.php
  15. 93 22
      modules/boonex/xero/plugins/guzzlehttp/guzzle/src/Cookie/CookieJar.php
  16. 25 5
      modules/boonex/xero/plugins/guzzlehttp/guzzle/src/Cookie/FileCookieJar.php
  17. 27 5
      modules/boonex/xero/plugins/guzzlehttp/guzzle/src/Cookie/SessionCookieJar.php
  18. 150 31
      modules/boonex/xero/plugins/guzzlehttp/guzzle/src/Cookie/SetCookie.php
  19. 0 2
      modules/boonex/xero/plugins/guzzlehttp/guzzle/src/Exception/ConnectException.php
  20. 4 0
      modules/boonex/xero/plugins/guzzlehttp/guzzle/src/Exception/RequestException.php
  21. 1738 143
      modules/boonex/xero/plugins/guzzlehttp/guzzle/src/Handler/CurlFactory.php
  22. 44 3
      modules/boonex/xero/plugins/guzzlehttp/guzzle/src/Handler/CurlHandler.php
  23. 1232 64
      modules/boonex/xero/plugins/guzzlehttp/guzzle/src/Handler/CurlMultiHandler.php
  24. 182 0
      modules/boonex/xero/plugins/guzzlehttp/guzzle/src/Handler/CurlShareHandleState.php
  25. 322 0
      modules/boonex/xero/plugins/guzzlehttp/guzzle/src/Handler/CurlVersion.php
  26. 29 3
      modules/boonex/xero/plugins/guzzlehttp/guzzle/src/Handler/EasyHandle.php
  27. 56 1
      modules/boonex/xero/plugins/guzzlehttp/guzzle/src/Handler/HeaderProcessor.php
  28. 198 0
      modules/boonex/xero/plugins/guzzlehttp/guzzle/src/Handler/HostValidator.php
  29. 1 2
      modules/boonex/xero/plugins/guzzlehttp/guzzle/src/Handler/MockHandler.php
  30. 30 0
      modules/boonex/xero/plugins/guzzlehttp/guzzle/src/Handler/Proxy.php
  31. 121 0
      modules/boonex/xero/plugins/guzzlehttp/guzzle/src/Handler/ProxyEnvironment.php
  32. 681 57
      modules/boonex/xero/plugins/guzzlehttp/guzzle/src/Handler/StreamHandler.php
  33. 84 0
      modules/boonex/xero/plugins/guzzlehttp/guzzle/src/Handler/TlsVersion.php
  34. 19 5
      modules/boonex/xero/plugins/guzzlehttp/guzzle/src/HandlerStack.php
  35. 9 4
      modules/boonex/xero/plugins/guzzlehttp/guzzle/src/MessageFormatter.php
  36. 3 3
      modules/boonex/xero/plugins/guzzlehttp/guzzle/src/Middleware.php
  37. 26 0
      modules/boonex/xero/plugins/guzzlehttp/guzzle/src/Multiplexing.php
  38. 13 1
      modules/boonex/xero/plugins/guzzlehttp/guzzle/src/Pool.php
  39. 1 1
      modules/boonex/xero/plugins/guzzlehttp/guzzle/src/PrepareBodyMiddleware.php
  40. 28 8
      modules/boonex/xero/plugins/guzzlehttp/guzzle/src/RedirectMiddleware.php
  41. 217 57
      modules/boonex/xero/plugins/guzzlehttp/guzzle/src/RequestOptions.php
  42. 7 1
      modules/boonex/xero/plugins/guzzlehttp/guzzle/src/RetryMiddleware.php
  43. 14 0
      modules/boonex/xero/plugins/guzzlehttp/guzzle/src/TransportSharing.php
  44. 606 48
      modules/boonex/xero/plugins/guzzlehttp/guzzle/src/Utils.php
  45. 108 7
      modules/boonex/xero/plugins/guzzlehttp/guzzle/src/functions.php
  46. 57 0
      modules/boonex/xero/plugins/guzzlehttp/promises/CHANGELOG.md
  47. 18 41
      modules/boonex/xero/plugins/guzzlehttp/promises/README.md
  48. 83 0
      modules/boonex/xero/plugins/guzzlehttp/promises/UPGRADING.md
  49. 12 9
      modules/boonex/xero/plugins/guzzlehttp/promises/composer.json
  50. 4 1
      modules/boonex/xero/plugins/guzzlehttp/promises/src/Coroutine.php
  51. 10 0
      modules/boonex/xero/plugins/guzzlehttp/promises/src/Create.php
  52. 23 0
      modules/boonex/xero/plugins/guzzlehttp/promises/src/Each.php
  53. 62 12
      modules/boonex/xero/plugins/guzzlehttp/promises/src/EachPromise.php
  54. 52 2
      modules/boonex/xero/plugins/guzzlehttp/promises/src/Utils.php
  55. 179 0
      modules/boonex/xero/plugins/guzzlehttp/psr7/CHANGELOG.md
  56. 98 44
      modules/boonex/xero/plugins/guzzlehttp/psr7/README.md
  57. 198 0
      modules/boonex/xero/plugins/guzzlehttp/psr7/UPGRADING.md
  58. 29 13
      modules/boonex/xero/plugins/guzzlehttp/psr7/composer.json
  59. 49 0
      modules/boonex/xero/plugins/guzzlehttp/psr7/src/AppendStream.php
  60. 45 0
      modules/boonex/xero/plugins/guzzlehttp/psr7/src/BufferStream.php
  61. 76 3
      modules/boonex/xero/plugins/guzzlehttp/psr7/src/CachingStream.php
  62. 9 0
      modules/boonex/xero/plugins/guzzlehttp/psr7/src/DroppingStream.php
  63. 48 3
      modules/boonex/xero/plugins/guzzlehttp/psr7/src/FnStream.php
  64. 49 3
      modules/boonex/xero/plugins/guzzlehttp/psr7/src/Header.php
  65. 35 4
      modules/boonex/xero/plugins/guzzlehttp/psr7/src/LimitStream.php
  66. 150 14
      modules/boonex/xero/plugins/guzzlehttp/psr7/src/Message.php
  67. 103 10
      modules/boonex/xero/plugins/guzzlehttp/psr7/src/MessageTrait.php
  68. 120 74
      modules/boonex/xero/plugins/guzzlehttp/psr7/src/MimeType.php
  69. 162 17
      modules/boonex/xero/plugins/guzzlehttp/psr7/src/MultipartStream.php
  70. 18 0
      modules/boonex/xero/plugins/guzzlehttp/psr7/src/NoSeekStream.php
  71. 65 14
      modules/boonex/xero/plugins/guzzlehttp/psr7/src/PumpStream.php
  72. 25 2
      modules/boonex/xero/plugins/guzzlehttp/psr7/src/Query.php
  73. 40 4
      modules/boonex/xero/plugins/guzzlehttp/psr7/src/Request.php
  74. 27 3
      modules/boonex/xero/plugins/guzzlehttp/psr7/src/Response.php
  75. 25 0
      modules/boonex/xero/plugins/guzzlehttp/psr7/src/Rfc3986.php
  76. 89 0
      modules/boonex/xero/plugins/guzzlehttp/psr7/src/Rfc7230.php
  77. 142 24
      modules/boonex/xero/plugins/guzzlehttp/psr7/src/ServerRequest.php
  78. 46 1
      modules/boonex/xero/plugins/guzzlehttp/psr7/src/Stream.php
  79. 45 0
      modules/boonex/xero/plugins/guzzlehttp/psr7/src/StreamDecoratorTrait.php
  80. 7 1
      modules/boonex/xero/plugins/guzzlehttp/psr7/src/StreamWrapper.php
  81. 198 57
      modules/boonex/xero/plugins/guzzlehttp/psr7/src/Uri.php
  82. 11 3
      modules/boonex/xero/plugins/guzzlehttp/psr7/src/UriComparator.php
  83. 45 14
      modules/boonex/xero/plugins/guzzlehttp/psr7/src/UriNormalizer.php
  84. 52 26
      modules/boonex/xero/plugins/guzzlehttp/psr7/src/UriResolver.php
  85. 288 41
      modules/boonex/xero/plugins/guzzlehttp/psr7/src/Utils.php
  86. 1 0
      modules/boonex/xero/plugins/league/oauth2-client/README.md
  87. 3 2
      modules/boonex/xero/plugins/league/oauth2-client/composer.json
  88. 1 1
      modules/boonex/xero/plugins/symfony/deprecation-contracts/composer.json
  89. 19 0
      modules/boonex/xero/plugins/symfony/polyfill-php80/LICENSE
  90. 115 0
      modules/boonex/xero/plugins/symfony/polyfill-php80/Php80.php
  91. 108 0
      modules/boonex/xero/plugins/symfony/polyfill-php80/PhpToken.php
  92. 25 0
      modules/boonex/xero/plugins/symfony/polyfill-php80/README.md
  93. 31 0
      modules/boonex/xero/plugins/symfony/polyfill-php80/Resources/stubs/Attribute.php
  94. 16 0
      modules/boonex/xero/plugins/symfony/polyfill-php80/Resources/stubs/PhpToken.php
  95. 20 0
      modules/boonex/xero/plugins/symfony/polyfill-php80/Resources/stubs/Stringable.php
  96. 16 0
      modules/boonex/xero/plugins/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php
  97. 16 0
      modules/boonex/xero/plugins/symfony/polyfill-php80/Resources/stubs/ValueError.php
  98. 42 0
      modules/boonex/xero/plugins/symfony/polyfill-php80/bootstrap.php
  99. 37 0
      modules/boonex/xero/plugins/symfony/polyfill-php80/composer.json

+ 6 - 0
.sonarcloud.properties

@@ -0,0 +1,6 @@
+# SonarCloud automatic analysis scope. Mirrors the project's UI settings and adds
+# modules/**/plugins/**: third-party libraries bundled by modules (Composer vendor
+# dirs, JS players), which UNA does not maintain.
+sonar.exclusions=modules/**/updates/**,modules/**/plugins/**,upgrade/**,patches/**,samples/**,scripts/**,tests/**
+sonar.cpd.exclusions=**
+sonar.coverage.exclusions=**

+ 1 - 1
modules/boonex/xero/composer.json

@@ -1,7 +1,7 @@
 {
 {
     "name": "unacms/xero",
     "name": "unacms/xero",
     "require": {
     "require": {
-        "xeroapi/xero-php-oauth2": "^10.0"
+        "xeroapi/xero-php-oauth2": "10.0.0"
     },
     },
     "config": {
     "config": {
         "vendor-dir": "plugins"
         "vendor-dir": "plugins"

+ 5 - 0
modules/boonex/xero/plugins/composer/autoload_classmap.php

@@ -6,5 +6,10 @@ $vendorDir = dirname(__DIR__);
 $baseDir = dirname($vendorDir);
 $baseDir = dirname($vendorDir);
 
 
 return array(
 return array(
+    'Attribute' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
     'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
     'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
+    'PhpToken' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php',
+    'Stringable' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Stringable.php',
+    'UnhandledMatchError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php',
+    'ValueError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/ValueError.php',
 );
 );

+ 2 - 1
modules/boonex/xero/plugins/composer/autoload_files.php

@@ -6,7 +6,8 @@ $vendorDir = dirname(__DIR__);
 $baseDir = dirname($vendorDir);
 $baseDir = dirname($vendorDir);
 
 
 return array(
 return array(
-    '7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php',
     '6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
     '6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
+    'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
+    '7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php',
     '37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',
     '37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',
 );
 );

+ 1 - 0
modules/boonex/xero/plugins/composer/autoload_psr4.php

@@ -7,6 +7,7 @@ $baseDir = dirname($vendorDir);
 
 
 return array(
 return array(
     'XeroAPI\\XeroPHP\\' => array($vendorDir . '/xeroapi/xero-php-oauth2/lib'),
     'XeroAPI\\XeroPHP\\' => array($vendorDir . '/xeroapi/xero-php-oauth2/lib'),
+    'Symfony\\Polyfill\\Php80\\' => array($vendorDir . '/symfony/polyfill-php80'),
     'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-factory/src', $vendorDir . '/psr/http-message/src'),
     'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-factory/src', $vendorDir . '/psr/http-message/src'),
     'Psr\\Http\\Client\\' => array($vendorDir . '/psr/http-client/src'),
     'Psr\\Http\\Client\\' => array($vendorDir . '/psr/http-client/src'),
     'League\\OAuth2\\Client\\' => array($vendorDir . '/league/oauth2-client/src'),
     'League\\OAuth2\\Client\\' => array($vendorDir . '/league/oauth2-client/src'),

+ 28 - 14
modules/boonex/xero/plugins/composer/autoload_static.php

@@ -7,75 +7,89 @@ namespace Composer\Autoload;
 class ComposerStaticInitae789faf34b271116449599e46798b3c
 class ComposerStaticInitae789faf34b271116449599e46798b3c
 {
 {
     public static $files = array (
     public static $files = array (
-        '7b11c4dc42b3b3023073cb14e519683c' => __DIR__ . '/..' . '/ralouphie/getallheaders/src/getallheaders.php',
         '6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php',
         '6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php',
+        'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php',
+        '7b11c4dc42b3b3023073cb14e519683c' => __DIR__ . '/..' . '/ralouphie/getallheaders/src/getallheaders.php',
         '37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php',
         '37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php',
     );
     );
 
 
     public static $prefixLengthsPsr4 = array (
     public static $prefixLengthsPsr4 = array (
-        'X' => 
+        'X' =>
         array (
         array (
             'XeroAPI\\XeroPHP\\' => 16,
             'XeroAPI\\XeroPHP\\' => 16,
         ),
         ),
-        'P' => 
+        'S' =>
+        array (
+            'Symfony\\Polyfill\\Php80\\' => 23,
+        ),
+        'P' =>
         array (
         array (
             'Psr\\Http\\Message\\' => 17,
             'Psr\\Http\\Message\\' => 17,
             'Psr\\Http\\Client\\' => 16,
             'Psr\\Http\\Client\\' => 16,
         ),
         ),
-        'L' => 
+        'L' =>
         array (
         array (
             'League\\OAuth2\\Client\\' => 21,
             'League\\OAuth2\\Client\\' => 21,
         ),
         ),
-        'G' => 
+        'G' =>
         array (
         array (
             'GuzzleHttp\\Psr7\\' => 16,
             'GuzzleHttp\\Psr7\\' => 16,
             'GuzzleHttp\\Promise\\' => 19,
             'GuzzleHttp\\Promise\\' => 19,
             'GuzzleHttp\\' => 11,
             'GuzzleHttp\\' => 11,
         ),
         ),
-        'F' => 
+        'F' =>
         array (
         array (
             'Firebase\\JWT\\' => 13,
             'Firebase\\JWT\\' => 13,
         ),
         ),
     );
     );
 
 
     public static $prefixDirsPsr4 = array (
     public static $prefixDirsPsr4 = array (
-        'XeroAPI\\XeroPHP\\' => 
+        'XeroAPI\\XeroPHP\\' =>
         array (
         array (
             0 => __DIR__ . '/..' . '/xeroapi/xero-php-oauth2/lib',
             0 => __DIR__ . '/..' . '/xeroapi/xero-php-oauth2/lib',
         ),
         ),
-        'Psr\\Http\\Message\\' => 
+        'Symfony\\Polyfill\\Php80\\' =>
+        array (
+            0 => __DIR__ . '/..' . '/symfony/polyfill-php80',
+        ),
+        'Psr\\Http\\Message\\' =>
         array (
         array (
             0 => __DIR__ . '/..' . '/psr/http-factory/src',
             0 => __DIR__ . '/..' . '/psr/http-factory/src',
             1 => __DIR__ . '/..' . '/psr/http-message/src',
             1 => __DIR__ . '/..' . '/psr/http-message/src',
         ),
         ),
-        'Psr\\Http\\Client\\' => 
+        'Psr\\Http\\Client\\' =>
         array (
         array (
             0 => __DIR__ . '/..' . '/psr/http-client/src',
             0 => __DIR__ . '/..' . '/psr/http-client/src',
         ),
         ),
-        'League\\OAuth2\\Client\\' => 
+        'League\\OAuth2\\Client\\' =>
         array (
         array (
             0 => __DIR__ . '/..' . '/league/oauth2-client/src',
             0 => __DIR__ . '/..' . '/league/oauth2-client/src',
         ),
         ),
-        'GuzzleHttp\\Psr7\\' => 
+        'GuzzleHttp\\Psr7\\' =>
         array (
         array (
             0 => __DIR__ . '/..' . '/guzzlehttp/psr7/src',
             0 => __DIR__ . '/..' . '/guzzlehttp/psr7/src',
         ),
         ),
-        'GuzzleHttp\\Promise\\' => 
+        'GuzzleHttp\\Promise\\' =>
         array (
         array (
             0 => __DIR__ . '/..' . '/guzzlehttp/promises/src',
             0 => __DIR__ . '/..' . '/guzzlehttp/promises/src',
         ),
         ),
-        'GuzzleHttp\\' => 
+        'GuzzleHttp\\' =>
         array (
         array (
             0 => __DIR__ . '/..' . '/guzzlehttp/guzzle/src',
             0 => __DIR__ . '/..' . '/guzzlehttp/guzzle/src',
         ),
         ),
-        'Firebase\\JWT\\' => 
+        'Firebase\\JWT\\' =>
         array (
         array (
             0 => __DIR__ . '/..' . '/firebase/php-jwt/src',
             0 => __DIR__ . '/..' . '/firebase/php-jwt/src',
         ),
         ),
     );
     );
 
 
     public static $classMap = array (
     public static $classMap = array (
+        'Attribute' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
         'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
         'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
+        'PhpToken' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php',
+        'Stringable' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Stringable.php',
+        'UnhandledMatchError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php',
+        'ValueError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/ValueError.php',
     );
     );
 
 
     public static function getInitializer(ClassLoader $loader)
     public static function getInitializer(ClassLoader $loader)

+ 146 - 48
modules/boonex/xero/plugins/composer/installed.json

@@ -68,26 +68,27 @@
         },
         },
         {
         {
             "name": "guzzlehttp/guzzle",
             "name": "guzzlehttp/guzzle",
-            "version": "7.9.3",
-            "version_normalized": "7.9.3.0",
+            "version": "7.15.5",
+            "version_normalized": "7.15.5.0",
             "source": {
             "source": {
                 "type": "git",
                 "type": "git",
                 "url": "https://github.com/guzzle/guzzle.git",
                 "url": "https://github.com/guzzle/guzzle.git",
-                "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77"
+                "reference": "ee80339fd9177ba44c49cdb653ff02a4d1106b9a"
             },
             },
             "dist": {
             "dist": {
                 "type": "zip",
                 "type": "zip",
-                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/7b2f29fe81dc4da0ca0ea7d42107a0845946ea77",
-                "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77",
+                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/ee80339fd9177ba44c49cdb653ff02a4d1106b9a",
+                "reference": "ee80339fd9177ba44c49cdb653ff02a4d1106b9a",
                 "shasum": ""
                 "shasum": ""
             },
             },
             "require": {
             "require": {
                 "ext-json": "*",
                 "ext-json": "*",
-                "guzzlehttp/promises": "^1.5.3 || ^2.0.3",
-                "guzzlehttp/psr7": "^2.7.0",
+                "guzzlehttp/promises": "^2.5.3",
+                "guzzlehttp/psr7": "^2.13.1",
                 "php": "^7.2.5 || ^8.0",
                 "php": "^7.2.5 || ^8.0",
                 "psr/http-client": "^1.0",
                 "psr/http-client": "^1.0",
-                "symfony/deprecation-contracts": "^2.2 || ^3.0"
+                "symfony/deprecation-contracts": "^2.5 || ^3.0",
+                "symfony/polyfill-php80": "^1.25"
             },
             },
             "provide": {
             "provide": {
                 "psr/http-client-implementation": "1.0"
                 "psr/http-client-implementation": "1.0"
@@ -95,9 +96,10 @@
             "require-dev": {
             "require-dev": {
                 "bamarni/composer-bin-plugin": "^1.8.2",
                 "bamarni/composer-bin-plugin": "^1.8.2",
                 "ext-curl": "*",
                 "ext-curl": "*",
-                "guzzle/client-integration-tests": "3.0.2",
+                "guzzle/client-integration-tests": "3.0.3",
+                "guzzlehttp/test-server": "^0.7",
                 "php-http/message-factory": "^1.1",
                 "php-http/message-factory": "^1.1",
-                "phpunit/phpunit": "^8.5.39 || ^9.6.20",
+                "phpunit/phpunit": "^8.5.52 || ^9.6.34",
                 "psr/log": "^1.1 || ^2.0 || ^3.0"
                 "psr/log": "^1.1 || ^2.0 || ^3.0"
             },
             },
             "suggest": {
             "suggest": {
@@ -105,7 +107,7 @@
                 "ext-intl": "Required for Internationalized Domain Name (IDN) support",
                 "ext-intl": "Required for Internationalized Domain Name (IDN) support",
                 "psr/log": "Required for using the Log middleware"
                 "psr/log": "Required for using the Log middleware"
             },
             },
-            "time": "2025-03-27T13:37:11+00:00",
+            "time": "2026-08-24T09:21:06+00:00",
             "type": "library",
             "type": "library",
             "extra": {
             "extra": {
                 "bamarni-bin": {
                 "bamarni-bin": {
@@ -177,7 +179,7 @@
             ],
             ],
             "support": {
             "support": {
                 "issues": "https://github.com/guzzle/guzzle/issues",
                 "issues": "https://github.com/guzzle/guzzle/issues",
-                "source": "https://github.com/guzzle/guzzle/tree/7.9.3"
+                "source": "https://github.com/guzzle/guzzle/tree/7.15.5"
             },
             },
             "funding": [
             "funding": [
                 {
                 {
@@ -197,27 +199,28 @@
         },
         },
         {
         {
             "name": "guzzlehttp/promises",
             "name": "guzzlehttp/promises",
-            "version": "2.2.0",
-            "version_normalized": "2.2.0.0",
+            "version": "2.5.3",
+            "version_normalized": "2.5.3.0",
             "source": {
             "source": {
                 "type": "git",
                 "type": "git",
                 "url": "https://github.com/guzzle/promises.git",
                 "url": "https://github.com/guzzle/promises.git",
-                "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c"
+                "reference": "cde49999552d185d64715fe9c1f77a2aadd2f9f1"
             },
             },
             "dist": {
             "dist": {
                 "type": "zip",
                 "type": "zip",
-                "url": "https://api.github.com/repos/guzzle/promises/zipball/7c69f28996b0a6920945dd20b3857e499d9ca96c",
-                "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c",
+                "url": "https://api.github.com/repos/guzzle/promises/zipball/cde49999552d185d64715fe9c1f77a2aadd2f9f1",
+                "reference": "cde49999552d185d64715fe9c1f77a2aadd2f9f1",
                 "shasum": ""
                 "shasum": ""
             },
             },
             "require": {
             "require": {
-                "php": "^7.2.5 || ^8.0"
+                "php": "^7.2.5 || ^8.0",
+                "symfony/deprecation-contracts": "^2.5 || ^3.0"
             },
             },
             "require-dev": {
             "require-dev": {
                 "bamarni/composer-bin-plugin": "^1.8.2",
                 "bamarni/composer-bin-plugin": "^1.8.2",
-                "phpunit/phpunit": "^8.5.39 || ^9.6.20"
+                "phpunit/phpunit": "^8.5.52 || ^9.6.34"
             },
             },
-            "time": "2025-03-27T13:27:01+00:00",
+            "time": "2026-08-24T09:11:28+00:00",
             "type": "library",
             "type": "library",
             "extra": {
             "extra": {
                 "bamarni-bin": {
                 "bamarni-bin": {
@@ -263,7 +266,7 @@
             ],
             ],
             "support": {
             "support": {
                 "issues": "https://github.com/guzzle/promises/issues",
                 "issues": "https://github.com/guzzle/promises/issues",
-                "source": "https://github.com/guzzle/promises/tree/2.2.0"
+                "source": "https://github.com/guzzle/promises/tree/2.5.3"
             },
             },
             "funding": [
             "funding": [
                 {
                 {
@@ -283,24 +286,26 @@
         },
         },
         {
         {
             "name": "guzzlehttp/psr7",
             "name": "guzzlehttp/psr7",
-            "version": "2.7.1",
-            "version_normalized": "2.7.1.0",
+            "version": "2.13.1",
+            "version_normalized": "2.13.1.0",
             "source": {
             "source": {
                 "type": "git",
                 "type": "git",
                 "url": "https://github.com/guzzle/psr7.git",
                 "url": "https://github.com/guzzle/psr7.git",
-                "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16"
+                "reference": "95e7828100de18b4e269fb1703be530082d5166d"
             },
             },
             "dist": {
             "dist": {
                 "type": "zip",
                 "type": "zip",
-                "url": "https://api.github.com/repos/guzzle/psr7/zipball/c2270caaabe631b3b44c85f99e5a04bbb8060d16",
-                "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16",
+                "url": "https://api.github.com/repos/guzzle/psr7/zipball/95e7828100de18b4e269fb1703be530082d5166d",
+                "reference": "95e7828100de18b4e269fb1703be530082d5166d",
                 "shasum": ""
                 "shasum": ""
             },
             },
             "require": {
             "require": {
                 "php": "^7.2.5 || ^8.0",
                 "php": "^7.2.5 || ^8.0",
                 "psr/http-factory": "^1.0",
                 "psr/http-factory": "^1.0",
                 "psr/http-message": "^1.1 || ^2.0",
                 "psr/http-message": "^1.1 || ^2.0",
-                "ralouphie/getallheaders": "^3.0"
+                "ralouphie/getallheaders": "^3.0",
+                "symfony/deprecation-contracts": "^2.5 || ^3.0",
+                "symfony/polyfill-php80": "^1.25"
             },
             },
             "provide": {
             "provide": {
                 "psr/http-factory-implementation": "1.0",
                 "psr/http-factory-implementation": "1.0",
@@ -308,13 +313,14 @@
             },
             },
             "require-dev": {
             "require-dev": {
                 "bamarni/composer-bin-plugin": "^1.8.2",
                 "bamarni/composer-bin-plugin": "^1.8.2",
-                "http-interop/http-factory-tests": "0.9.0",
-                "phpunit/phpunit": "^8.5.39 || ^9.6.20"
+                "http-interop/http-factory-tests": "1.1.0",
+                "jshttp/mime-db": "1.54.0.1",
+                "phpunit/phpunit": "^8.5.52 || ^9.6.34"
             },
             },
             "suggest": {
             "suggest": {
                 "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
                 "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
             },
             },
-            "time": "2025-03-27T12:30:47+00:00",
+            "time": "2026-08-24T09:13:11+00:00",
             "type": "library",
             "type": "library",
             "extra": {
             "extra": {
                 "bamarni-bin": {
                 "bamarni-bin": {
@@ -382,7 +388,7 @@
             ],
             ],
             "support": {
             "support": {
                 "issues": "https://github.com/guzzle/psr7/issues",
                 "issues": "https://github.com/guzzle/psr7/issues",
-                "source": "https://github.com/guzzle/psr7/tree/2.7.1"
+                "source": "https://github.com/guzzle/psr7/tree/2.13.1"
             },
             },
             "funding": [
             "funding": [
                 {
                 {
@@ -402,23 +408,24 @@
         },
         },
         {
         {
             "name": "league/oauth2-client",
             "name": "league/oauth2-client",
-            "version": "2.8.1",
-            "version_normalized": "2.8.1.0",
+            "version": "2.9.1",
+            "version_normalized": "2.9.1.0",
             "source": {
             "source": {
                 "type": "git",
                 "type": "git",
                 "url": "https://github.com/thephpleague/oauth2-client.git",
                 "url": "https://github.com/thephpleague/oauth2-client.git",
-                "reference": "9df2924ca644736c835fc60466a3a60390d334f9"
+                "reference": "8cedfef9d01a8d1fd2ecfabb41734b17592c4b71"
             },
             },
             "dist": {
             "dist": {
                 "type": "zip",
                 "type": "zip",
-                "url": "https://api.github.com/repos/thephpleague/oauth2-client/zipball/9df2924ca644736c835fc60466a3a60390d334f9",
-                "reference": "9df2924ca644736c835fc60466a3a60390d334f9",
+                "url": "https://api.github.com/repos/thephpleague/oauth2-client/zipball/8cedfef9d01a8d1fd2ecfabb41734b17592c4b71",
+                "reference": "8cedfef9d01a8d1fd2ecfabb41734b17592c4b71",
                 "shasum": ""
                 "shasum": ""
             },
             },
             "require": {
             "require": {
                 "ext-json": "*",
                 "ext-json": "*",
-                "guzzlehttp/guzzle": "^6.5.8 || ^7.4.5",
-                "php": "^7.1 || >=8.0.0 <8.5.0"
+                "guzzlehttp/guzzle": "^6.5.8 || ^7.8.2 || ^8.0",
+                "guzzlehttp/psr7": "^1.9.1 || ^2.6.3 || ^3.0",
+                "php": "^7.1 || >=8.0.0 <8.6.0"
             },
             },
             "require-dev": {
             "require-dev": {
                 "mockery/mockery": "^1.3.5",
                 "mockery/mockery": "^1.3.5",
@@ -426,7 +433,7 @@
                 "phpunit/phpunit": "^7 || ^8 || ^9 || ^10 || ^11",
                 "phpunit/phpunit": "^7 || ^8 || ^9 || ^10 || ^11",
                 "squizlabs/php_codesniffer": "^3.11"
                 "squizlabs/php_codesniffer": "^3.11"
             },
             },
-            "time": "2025-02-26T04:37:30+00:00",
+            "time": "2026-09-16T13:14:31+00:00",
             "type": "library",
             "type": "library",
             "installation-source": "dist",
             "installation-source": "dist",
             "autoload": {
             "autoload": {
@@ -464,7 +471,7 @@
             ],
             ],
             "support": {
             "support": {
                 "issues": "https://github.com/thephpleague/oauth2-client/issues",
                 "issues": "https://github.com/thephpleague/oauth2-client/issues",
-                "source": "https://github.com/thephpleague/oauth2-client/tree/2.8.1"
+                "source": "https://github.com/thephpleague/oauth2-client/tree/2.9.1"
             },
             },
             "install-path": "../league/oauth2-client"
             "install-path": "../league/oauth2-client"
         },
         },
@@ -686,23 +693,23 @@
         },
         },
         {
         {
             "name": "symfony/deprecation-contracts",
             "name": "symfony/deprecation-contracts",
-            "version": "v3.6.0",
-            "version_normalized": "3.6.0.0",
+            "version": "v3.7.1",
+            "version_normalized": "3.7.1.0",
             "source": {
             "source": {
                 "type": "git",
                 "type": "git",
                 "url": "https://github.com/symfony/deprecation-contracts.git",
                 "url": "https://github.com/symfony/deprecation-contracts.git",
-                "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62"
+                "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d"
             },
             },
             "dist": {
             "dist": {
                 "type": "zip",
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62",
-                "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62",
+                "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/f3202fa1b5097b0af062dc978b32ecf63404e31d",
+                "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d",
                 "shasum": ""
                 "shasum": ""
             },
             },
             "require": {
             "require": {
                 "php": ">=8.1"
                 "php": ">=8.1"
             },
             },
-            "time": "2024-09-25T14:21:43+00:00",
+            "time": "2026-06-05T06:23:12+00:00",
             "type": "library",
             "type": "library",
             "extra": {
             "extra": {
                 "thanks": {
                 "thanks": {
@@ -710,7 +717,7 @@
                     "name": "symfony/contracts"
                     "name": "symfony/contracts"
                 },
                 },
                 "branch-alias": {
                 "branch-alias": {
-                    "dev-main": "3.6-dev"
+                    "dev-main": "3.7-dev"
                 }
                 }
             },
             },
             "installation-source": "dist",
             "installation-source": "dist",
@@ -736,7 +743,7 @@
             "description": "A generic function and convention to trigger deprecation notices",
             "description": "A generic function and convention to trigger deprecation notices",
             "homepage": "https://symfony.com",
             "homepage": "https://symfony.com",
             "support": {
             "support": {
-                "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0"
+                "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.1"
             },
             },
             "funding": [
             "funding": [
                 {
                 {
@@ -747,6 +754,10 @@
                     "url": "https://github.com/fabpot",
                     "url": "https://github.com/fabpot",
                     "type": "github"
                     "type": "github"
                 },
                 },
+                {
+                    "url": "https://github.com/nicolas-grekas",
+                    "type": "github"
+                },
                 {
                 {
                     "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
                     "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
                     "type": "tidelift"
                     "type": "tidelift"
@@ -754,6 +765,93 @@
             ],
             ],
             "install-path": "../symfony/deprecation-contracts"
             "install-path": "../symfony/deprecation-contracts"
         },
         },
+        {
+            "name": "symfony/polyfill-php80",
+            "version": "v1.43.0",
+            "version_normalized": "1.43.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-php80.git",
+                "reference": "9c6a5d6b01ca51d486c813e9a9c0ed55f208bb74"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/9c6a5d6b01ca51d486c813e9a9c0ed55f208bb74",
+                "reference": "9c6a5d6b01ca51d486c813e9a9c0ed55f208bb74",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.2"
+            },
+            "time": "2026-09-25T15:50:05+00:00",
+            "type": "library",
+            "extra": {
+                "thanks": {
+                    "url": "https://github.com/symfony/polyfill",
+                    "name": "symfony/polyfill"
+                }
+            },
+            "installation-source": "dist",
+            "autoload": {
+                "files": [
+                    "bootstrap.php"
+                ],
+                "psr-4": {
+                    "Symfony\\Polyfill\\Php80\\": ""
+                },
+                "classmap": [
+                    "Resources/stubs"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Ion Bazan",
+                    "email": "ion.bazan@gmail.com"
+                },
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-php80/tree/v1.43.0"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://github.com/nicolas-grekas",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "install-path": "../symfony/polyfill-php80"
+        },
         {
         {
             "name": "xeroapi/xero-php-oauth2",
             "name": "xeroapi/xero-php-oauth2",
             "version": "10.0.0",
             "version": "10.0.0",

+ 30 - 21
modules/boonex/xero/plugins/composer/installed.php

@@ -1,9 +1,9 @@
 <?php return array(
 <?php return array(
     'root' => array(
     'root' => array(
         'name' => 'unacms/xero',
         'name' => 'unacms/xero',
-        'pretty_version' => 'dev-master',
-        'version' => 'dev-master',
-        'reference' => '0e4f0eb6bc2595719518d133266a527ee87f32d7',
+        'pretty_version' => '1.0.0+no-version-set',
+        'version' => '1.0.0.0',
+        'reference' => null,
         'type' => 'library',
         'type' => 'library',
         'install_path' => __DIR__ . '/../../',
         'install_path' => __DIR__ . '/../../',
         'aliases' => array(),
         'aliases' => array(),
@@ -20,36 +20,36 @@
             'dev_requirement' => false,
             'dev_requirement' => false,
         ),
         ),
         'guzzlehttp/guzzle' => array(
         'guzzlehttp/guzzle' => array(
-            'pretty_version' => '7.9.3',
-            'version' => '7.9.3.0',
-            'reference' => '7b2f29fe81dc4da0ca0ea7d42107a0845946ea77',
+            'pretty_version' => '7.15.5',
+            'version' => '7.15.5.0',
+            'reference' => 'ee80339fd9177ba44c49cdb653ff02a4d1106b9a',
             'type' => 'library',
             'type' => 'library',
             'install_path' => __DIR__ . '/../guzzlehttp/guzzle',
             'install_path' => __DIR__ . '/../guzzlehttp/guzzle',
             'aliases' => array(),
             'aliases' => array(),
             'dev_requirement' => false,
             'dev_requirement' => false,
         ),
         ),
         'guzzlehttp/promises' => array(
         'guzzlehttp/promises' => array(
-            'pretty_version' => '2.2.0',
-            'version' => '2.2.0.0',
-            'reference' => '7c69f28996b0a6920945dd20b3857e499d9ca96c',
+            'pretty_version' => '2.5.3',
+            'version' => '2.5.3.0',
+            'reference' => 'cde49999552d185d64715fe9c1f77a2aadd2f9f1',
             'type' => 'library',
             'type' => 'library',
             'install_path' => __DIR__ . '/../guzzlehttp/promises',
             'install_path' => __DIR__ . '/../guzzlehttp/promises',
             'aliases' => array(),
             'aliases' => array(),
             'dev_requirement' => false,
             'dev_requirement' => false,
         ),
         ),
         'guzzlehttp/psr7' => array(
         'guzzlehttp/psr7' => array(
-            'pretty_version' => '2.7.1',
-            'version' => '2.7.1.0',
-            'reference' => 'c2270caaabe631b3b44c85f99e5a04bbb8060d16',
+            'pretty_version' => '2.13.1',
+            'version' => '2.13.1.0',
+            'reference' => '95e7828100de18b4e269fb1703be530082d5166d',
             'type' => 'library',
             'type' => 'library',
             'install_path' => __DIR__ . '/../guzzlehttp/psr7',
             'install_path' => __DIR__ . '/../guzzlehttp/psr7',
             'aliases' => array(),
             'aliases' => array(),
             'dev_requirement' => false,
             'dev_requirement' => false,
         ),
         ),
         'league/oauth2-client' => array(
         'league/oauth2-client' => array(
-            'pretty_version' => '2.8.1',
-            'version' => '2.8.1.0',
-            'reference' => '9df2924ca644736c835fc60466a3a60390d334f9',
+            'pretty_version' => '2.9.1',
+            'version' => '2.9.1.0',
+            'reference' => '8cedfef9d01a8d1fd2ecfabb41734b17592c4b71',
             'type' => 'library',
             'type' => 'library',
             'install_path' => __DIR__ . '/../league/oauth2-client',
             'install_path' => __DIR__ . '/../league/oauth2-client',
             'aliases' => array(),
             'aliases' => array(),
@@ -110,18 +110,27 @@
             'dev_requirement' => false,
             'dev_requirement' => false,
         ),
         ),
         'symfony/deprecation-contracts' => array(
         'symfony/deprecation-contracts' => array(
-            'pretty_version' => 'v3.6.0',
-            'version' => '3.6.0.0',
-            'reference' => '63afe740e99a13ba87ec199bb07bbdee937a5b62',
+            'pretty_version' => 'v3.7.1',
+            'version' => '3.7.1.0',
+            'reference' => 'f3202fa1b5097b0af062dc978b32ecf63404e31d',
             'type' => 'library',
             'type' => 'library',
             'install_path' => __DIR__ . '/../symfony/deprecation-contracts',
             'install_path' => __DIR__ . '/../symfony/deprecation-contracts',
             'aliases' => array(),
             'aliases' => array(),
             'dev_requirement' => false,
             'dev_requirement' => false,
         ),
         ),
+        'symfony/polyfill-php80' => array(
+            'pretty_version' => 'v1.43.0',
+            'version' => '1.43.0.0',
+            'reference' => '9c6a5d6b01ca51d486c813e9a9c0ed55f208bb74',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../symfony/polyfill-php80',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
         'unacms/xero' => array(
         'unacms/xero' => array(
-            'pretty_version' => 'dev-master',
-            'version' => 'dev-master',
-            'reference' => '0e4f0eb6bc2595719518d133266a527ee87f32d7',
+            'pretty_version' => '1.0.0+no-version-set',
+            'version' => '1.0.0.0',
+            'reference' => null,
             'type' => 'library',
             'type' => 'library',
             'install_path' => __DIR__ . '/../../',
             'install_path' => __DIR__ . '/../../',
             'aliases' => array(),
             'aliases' => array(),

+ 2 - 3
modules/boonex/xero/plugins/composer/platform_check.php

@@ -19,8 +19,7 @@ if ($issues) {
             echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
             echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
         }
         }
     }
     }
-    trigger_error(
-        'Composer detected issues in your platform: ' . implode(' ', $issues),
-        E_USER_ERROR
+    throw new \RuntimeException(
+        'Composer detected issues in your platform: ' . implode(' ', $issues)
     );
     );
 }
 }

+ 411 - 2
modules/boonex/xero/plugins/guzzlehttp/guzzle/CHANGELOG.md

@@ -3,6 +3,415 @@
 Please refer to [UPGRADING](UPGRADING.md) guide for upgrading to a major version.
 Please refer to [UPGRADING](UPGRADING.md) guide for upgrading to a major version.
 
 
 
 
+## 7.15.5 - 2026-08-24
+
+### Changed
+
+- Adjusted `guzzlehttp/psr7` version constraint to `^2.13.1`
+- Adjusted `guzzlehttp/promises` version constraint to `^2.5.3`
+
+## 7.15.4 - 2026-08-24
+
+### Added
+
+- Added support for PHP 8.6
+
+### Changed
+
+- Replace the deprecated spl_object_hash() in handler stack debug output for PHP 8.6
+
+## 7.15.3 - 2026-08-05
+
+### Changed
+
+- Adjusted `guzzlehttp/promises` version constraint to `^2.5.2`
+
+### Fixed
+
+- Fail a cURL multi handler wait with an attributable error when the transfer is no longer tracked
+- Fix `StreamHandler` resolving numeric IPv4 hosts differently from cURL handlers on macOS and Windows
+- Fix `StreamHandler` TLS peer names and proxy authorities for numeric IPv4 hosts on all platforms
+- Settle a cURL multi handler transfer displaced by a request reusing its native handle ID
+
+
+## 7.15.2 - 2026-07-26
+
+### Security
+
+- Reject non-printable-ASCII and percent-escaped URI hosts and `Host` headers (GHSA-v5mv-p594-2x33)
+- Reject request URI hosts that contain a URI authority delimiter (GHSA-v5mv-p594-2x33)
+- Reject numeric-looking URI hosts with trailing dots, read as IPv4 addresses (GHSA-v5mv-p594-2x33)
+- Treat numeric-in-any-base and percent-escaped cookie domains as exact-match-only (GHSA-f7vp-7xgx-4w4r)
+- Regenerate a derived `Host` header after client URI rewrites (GHSA-v5mv-p594-2x33)
+
+### Fixed
+
+- Preserve `RequestException` when the stream handler rejects a request before opening a stream
+
+
+## 7.15.1 - 2026-07-18
+
+### Security
+
+- Preserve host-only cookie scope and require explicit persistence markers (GHSA-wm3w-8rrp-j577)
+- Bound response cookie admission and generated `Cookie` headers (GHSA-f283-ghqc-fg79)
+- Exclude URI fragments from `Referer` headers generated for redirects (GHSA-h95v-h523-3mw8)
+
+
+## 7.15.0 - 2026-07-17
+
+### Added
+
+- Added `Multiplexing::NONE` support as a client, cURL multi handler, and conditional request option
+
+### Changed
+
+- Adjusted `guzzlehttp/psr7` version constraint to `^2.13`
+- Use locale-independent ASCII folding for all case normalization and comparison
+- Bound cURL upload reads to the declared `Content-Length`
+- Sanitize the cURL error text exposed through exception handler context
+- Fail closed when a named cURL multi connection cap cannot be applied
+- Reject the request-level `CURLOPT_SHARE` cURL option when named connection caps are configured
+- Strengthen old-libcurl SOCKS isolation for raw `CURLOPT_PRE_PROXY` and opaque share handles
+- Isolate HTTP proxy tunnels from opaque shared connection caches
+- Trigger runtime deprecations for previously deprecated functionality in 7.1.0
+
+### Deprecated
+
+- Deprecated `Utils::jsonDecode()` and `Utils::jsonEncode()` in favor of native JSON functions
+- Deprecated passing `CURLMOPT_PIPELINING` in the cURL multi handler `options` array
+- Deprecated passing `CURLOPT_PROXYHEADER` without cURL proxy header separation support
+
+### Fixed
+
+- Defer cURL requests created from multi callbacks until native execution unwinds
+- Fail synchronous waits from native cURL callbacks promptly instead of self-deadlocking
+- Guard cURL multi handle removal against progress callbacks re-entering the handler
+- Scope promise waits on the cURL multi handler to the awaited transfer
+- Strip `Content-Length` and `Transfer-Encoding` when redirects discard the request body
+- Stop re-applying the `delay` request option to followed redirects
+
+
+## 7.14.2 - 2026-07-14
+
+### Security
+
+- Prevent first-class and proxy URL credentials from reaching origins (GHSA-94pj-82f3-465w)
+
+
+## 7.14.1 - 2026-07-13
+
+### Changed
+
+- Adjusted `guzzlehttp/psr7` version constraint to `^2.12.5`
+
+### Fixed
+
+- Fail closed when a proxy tunnel isolation cURL option cannot be applied
+- Normalize Stringable proxy credential values before computing connection-reuse section signatures
+- Restore conservative credential redaction for unparseable proxies with multiple `@` separators
+- Redact request URI credentials from the stream handler connection error message
+- Reject enabled response streaming (`stream => true`) on cap-configured stream handlers
+- Distinguish CurlMultiHandler and StreamHandler outcomes in connection-cap custom-handler guidance
+- Reject raw cURL options that conflict with explicit multiplexing guarantees
+- Stop explicit multiplexing conflict checks faulting on non-array cURL multi `options` values
+- Reject required multiplexing when the final `CURLOPT_HTTPAUTH` mask permits NTLM
+- Require an integer `CURLMOPT_PIPELINING` when combined with explicit multiplexing
+- Check the required multiplexing cleartext proxy rule against the final cURL configuration
+- Bound cURL multi handler blocking selects by the earliest pending request delay
+- Stop synchronous cURL multi handler waits blocking on other transfers once the target has settled
+- Stop cURL multi completion processing double-settling promises canceled from completion callbacks
+- Run ready promise queue tasks before sleeping for delayed cURL multi requests
+- Avoid integer overflow in cURL multi delay timing on 32-bit platforms
+- Roll back failed cURL multi handle attachment instead of leaving requests pending
+- Release the cURL easy handle when the `on_stats` callback throws
+- Normalize response trailer field names to lowercase with values in wire order
+- Retain response trailers only when an `on_trailers` callback is configured
+- Validate the `on_trailers` callback before starting a cURL transfer
+- Reject the `on_trailers` request option on the stream handler, which cannot observe trailers
+- Match cookies, proxy schemes, auth types, and header names with locale-independent ASCII folding
+- Reject proxy option values that Guzzle cannot classify identically to ext-curl
+
+
+## 7.14.0 - 2026-07-08
+
+### Added
+
+- Added the `on_trailers` request option to expose parsed HTTP response trailers
+- Added the `multiplex` request option with `Multiplexing::*` modes to control or require HTTP/2 multiplexing
+- Added rejection of explicit `multiplex` requests when `CURLMOPT_PIPELINING` disables multiplexing
+- Added the `max_host_connections` and `max_total_connections` client and cURL multi handler options
+
+### Changed
+
+- Redirects that discard the request body no longer require it to be rewindable
+- Synchronous cURL multi handler requests no longer wait for other queued transfers
+- Section SOCKS proxy connections by credentials on libcurl before 7.69.0
+- Reject request-level `CURLOPT_SHARE` when combined with authenticated SOCKS proxy configuration
+- Redact proxy userinfo containing raw control bytes in cURL errors
+- Check linked curl/libcurl NTLM support before applying NTLM auth
+- Clarify that NTLM is deprecated by both Guzzle and curl/libcurl
+- Remove deprecation for the raw cURL `CURLOPT_CERTINFO` option
+- Warn when a cURL multi option cannot be applied
+
+### Deprecated
+
+- Deprecate the raw `CURLOPT_PIPEWAIT` cURL option in favour of the `multiplex` request option
+- Deprecate unknown handler constructor options
+- Deprecate invalid `select_timeout` cURL multi handler option values
+- Deprecate raw cURL multi connection cap options in favour of the named options
+
+
+## 7.13.3 - 2026-07-08
+
+### Changed
+
+- Adjusted `guzzlehttp/promises` version constraint to `^2.5.1`
+- Adjusted `guzzlehttp/psr7` version constraint to `^2.12.4`
+- Pass explicit trim characters ahead of the PHP 8.6 trim default change
+
+### Fixed
+
+- Stop matching cookie domains against hosts with a trailing newline
+- Reject HTTP status codes and certificate type extensions with a trailing newline
+- Treat PCRE engine failures as invalid cookie names during cookie validation
+- Report PCRE engine failures when formatting log messages
+- Report PCRE engine failures when splitting `no_proxy` values
+
+
+## 7.13.2 - 2026-07-05
+
+### Fixed
+
+- Stop the cURL multi handler busy-waiting on request delays shorter than one second
+- Stop cURL HEAD requests with request bodies hanging on responses that declare a content length
+- The cURL handler no longer transmits request bodies on HEAD requests
+- Preserve response headers when a response includes HTTP trailers
+- Harden cURL response header block detection when HTTP trailers are received
+- Corrected the PSR-7 class names in the Pool iterator exception
+- Redirect body rewind failures no longer leak a bare `RuntimeException`
+
+
+## 7.13.1 - 2026-06-29
+
+### Fixed
+
+- Allow middleware to rewrite partial URIs before transports validate them
+
+
+## 7.13.0 - 2026-06-29
+
+### Added
+
+- Added the `crypto_method_max` request option to cap the maximum TLS protocol version
+- Added HTTP QUERY redirect support, preserving method and body on 301 and 302
+
+### Changed
+
+- Section proxy tunnel connection reuse by credential so distinct credentials never share a tunnel
+- Isolate concurrent foreign cURL proxy tunnels added while another owner's tunnel is active
+- Route credentialed HTTP(S) proxy Proxy-Authorization headers through cURL proxy header handling
+- Reject request-level `CURLOPT_SHARE` when combined with authenticated HTTP/HTTPS proxy tunnel configuration
+- Remove deprecation for raw cURL `CURLOPT_PREREQFUNCTION` callbacks when defined by PHP cURL
+- Route TLS 1.2 `crypto_method` requests to the stream handler when cURL cannot select TLS 1.2
+- Reject final request URIs missing a scheme or host before transfer
+
+### Deprecated
+
+- Deprecate invalid protocols, force_ip_resolve, delay, cookies, and allow_redirects values
+
+
+## 7.12.3 - 2026-06-23
+
+### Changed
+
+- Adjusted `guzzlehttp/psr7` version constraint to `^2.12.3`
+
+### Security
+
+- Treat IP and numeric cookie domains as exact-match-only (GHSA-g446-98w2-8p5w)
+
+
+## 7.12.2 - 2026-06-23
+
+### Fixed
+
+- Clamp out-of-range `Max-Age` so a very large value no longer overflows to an already-expired timestamp
+- Use strict comparison in `CookieJar` conflict resolution so distinct numeric-string names don't overwrite
+- Store a cookie whose `Domain` has a trailing dot on the origin host instead of silently discarding it
+- Fix `StreamHandler` hard-failing on bracketed IPv6 literal hosts when `force_ip_resolve` is set
+- Use strict cookie `Path` comparison so `CookieJar::clear()` with a numeric path keeps a distinct-path cookie
+- Fixed cookie handling for falsey `Domain`, `Max-Age`, path, and name values
+- Fixed `decode_content` handling for falsey string values
+- Fixed deprecated request option values reaching built-in handlers before normalization
+
+
+## 7.12.1 - 2026-06-18
+
+### Changed
+
+- Adjusted `guzzlehttp/psr7` version constraint to `^2.12.1`
+
+### Fixed
+
+- Reject proxy URLs with a malformed scheme in the cURL handlers instead of letting libcurl mishandle them
+
+### Security
+
+- Reject HTTPS proxies when the installed libcurl lacks HTTPS-proxy support (GHSA-wpwq-4j6v-78m3)
+- Reject dot-only cookie `Domain` attributes as match-all (GHSA-cwxw-98qj-8qjx)
+
+
+## 7.12.0 - 2026-06-16
+
+### Added
+
+- Added `RequestOptions` constants for `curl`, `retries`, and `stream_context`
+
+### Changed
+
+- Adjusted `guzzlehttp/psr7` version constraint to `^2.12`
+- Constrain cURL transport sharing to safe libcurl DNS and SSL session support
+- Resolve proxy environment variables in the cURL handlers; libcurl no longer reads the environment itself
+- Ignore proxy environment variables when the `proxy` request option makes a decision
+- Disable proxy environment variables on Windows SAPIs other than CLI (httpoxy hardening)
+- Redact proxy credentials from cURL handler error messages, following `Psr7\Utils::redactUserInfo()`
+- Normalize no-proxy domain and IP literal matching across the cURL and stream handlers
+
+### Deprecated
+
+- Deprecated the request-level `handler` option, which will be ignored in 8.0
+- Deprecated raw cURL request options outside the built-in cURL handlers' allow-list
+- Deprecated the `CURLOPT_PROXYTYPE` cURL request option; set the proxy type via a scheme-prefixed proxy URL
+- Deprecated PHP stream context options outside the built-in stream handler allow-list
+- Deprecated passing `ntlm` as a built-in `auth` type
+- Deprecated `Utils::describeType()`
+- Deprecated non-finite floats in the `query` and `form_params` options; 8.0 rejects them
+- Deprecated non-string scalar values in the `body` option; 8.0 rejects them
+
+### Fixed
+
+- Fix cURL TLS and HTTP/2 capability detection using libcurl feature checks
+- Fix proxy `no` list matches being re-proxied through environment-configured proxies by libcurl
+- Fix `no` list and `NO_PROXY` matching to support IP CIDR ranges, matching libcurl
+- Fix the stream handler not applying scheme-less proxies and their credentials
+
+
+## 7.11.2 - 2026-06-12
+
+### Fixed
+
+- Fixed non-finite float values emitting coercion warnings on PHP 8.5
+
+
+## 7.11.1 - 2026-06-07
+
+### Fixed
+
+- Ignore request-level `transport_sharing`, matching other unknown request options
+
+
+## 7.11.0 - 2026-06-02
+
+### Added
+
+- Added support for providing the `proxy` request option's `no` value as a comma-delimited string
+- Added the `protocols` request option to restrict allowed URI schemes for request transfers
+- Added `cert_type` and `ssl_key_type` request options for TLS certificate and private-key file types
+- Added PHP stream handler support for the `ssl_key` request option
+- Added transport sharing via the `transport_sharing` client and cURL handler options
+
+### Changed
+
+- Adjusted `guzzlehttp/promises` version constraint to `^2.5`
+- Adjusted `guzzlehttp/psr7` version constraint to `^2.11`
+- Allowed domainless `SetCookie` instances to be stored without wildcard request matching
+- Changed no-proxy matching to respect request ports for host-and-port rules
+- Prevented `CurlMultiHandler` destructors from throwing during cleanup
+- Improved invalid response handling across handlers
+
+### Deprecated
+
+- Deprecated non-iterable `Pool` request collections, which will be rejected in 8.0
+- Deprecated non-uppercase easy request methods; 8.0 preserves method casing
+- Deprecated non-string `headers` request option values, which will be rejected in 8.0
+- Deprecated empty `headers` request option value arrays, which will be rejected in 8.0
+- Deprecated empty and malformed request protocol versions, which will be rejected in 8.0
+- Deprecated conflicting raw cURL request options, including `CURLOPT_SHARE`, which will be rejected in 8.0
+- Deprecated scalar-coerced `idn_conversion` request option values, which will be rejected in 8.0
+- Deprecated invalid documented request option value types, which will be rejected in 8.0
+- Deprecated selected request options ignored by incompatible built-in handlers, which will be rejected in 8.0
+- Deprecated `RequestException::wrapException()`, which will be removed in 8.0
+- Deprecated `RetryMiddleware::exponentialDelay()`, which will be removed in 8.0
+
+
+## 7.10.6 - 2026-06-01
+
+### Fixed
+
+- `CurlMultiHandler` now rejects the promise when `CurlFactory::finish()` throws, preserving sibling transfers
+- `SetCookie` now normalizes unparseable `Expires` values to `null` instead of `false`
+- Fix stream handler decoded `gzip`/`deflate` truncation by dropping invalid `Content-Length`
+
+
+## 7.10.5 - 2026-05-27
+
+### Fixed
+
+- Defer cURL multi cancellation cleanup until after progress callbacks return
+- Classify additional stream handler connection failures as `ConnectException`
+
+
+## 7.10.4 - 2026-05-22
+
+### Fixed
+
+- Fix IPv6 literal matching in no-proxy rules
+- Handle cURL multi completion messages without handles after cancelled transfers
+- Fix magic client request methods such as `options()` to uppercase inferred HTTP methods
+
+
+## 7.10.3 - 2026-05-20
+
+### Fixed
+
+- Fail clearly when an HTTP response header line is invalid
+- Remove middleware by name when the name is also a callable string
+- Treat empty request protocol versions as HTTP/1.1
+
+
+## 7.10.2 - 2026-05-20
+
+### Fixed
+
+- Normalize HTTP version request options before applying them to PSR-7 requests
+- Use string values for headers generated by request preparation and response decoding
+
+
+## 7.10.1 - 2026-05-19
+
+### Fixed
+
+- Fail clearly when cURL options cannot be applied
+- Fail clearly when the certificate option is malformed
+- Fail clearly when JSON decode depth is invalid
+- Fail clearly when session cookie data is malformed
+- Fail clearly when the stream progress option is not callable
+- Prevent response creation failures from exposing stale cURL responses
+
+
+## 7.10.0 - 2025-08-23
+
+### Added
+
+- Support for PHP 8.5
+
+### Changed
+
+- Adjusted `guzzlehttp/promises` version constraint to `^2.3`
+- Adjusted `guzzlehttp/psr7` version constraint to `^2.8`
+
+
 ## 7.9.3 - 2025-03-27
 ## 7.9.3 - 2025-03-27
 
 
 ### Changed
 ### Changed
@@ -923,7 +1332,7 @@ interfaces.
 ## 4.0.0 - 2014-03-29
 ## 4.0.0 - 2014-03-29
 
 
 * For information on changes and upgrading, see:
 * For information on changes and upgrading, see:
-  https://github.com/guzzle/guzzle/blob/master/UPGRADING.md#3x-to-40
+  https://github.com/guzzle/guzzle/blob/4.x/UPGRADING.md#3x-to-40
 * Added `GuzzleHttp\batch()` as a convenience function for sending requests in
 * Added `GuzzleHttp\batch()` as a convenience function for sending requests in
   parallel without needing to write asynchronous code.
   parallel without needing to write asynchronous code.
 * Restructured how events are added to `GuzzleHttp\ClientInterface::sendAll()`.
 * Restructured how events are added to `GuzzleHttp\ClientInterface::sendAll()`.
@@ -956,7 +1365,7 @@ interfaces.
 
 
 ## 4.0.0-rc.1 - 2014-03-15
 ## 4.0.0-rc.1 - 2014-03-15
 
 
-* See https://github.com/guzzle/guzzle/blob/master/UPGRADING.md#3x-to-40
+* See https://github.com/guzzle/guzzle/blob/4.x/UPGRADING.md#3x-to-40
 
 
 ## 3.8.1 - 2014-01-28
 ## 3.8.1 - 2014-01-28
 
 

+ 7 - 7
modules/boonex/xero/plugins/guzzlehttp/guzzle/README.md

@@ -42,7 +42,7 @@ $promise->wait();
 
 
 We use GitHub issues only to discuss bugs and new features. For support please refer to:
 We use GitHub issues only to discuss bugs and new features. For support please refer to:
 
 
-- [Documentation](https://docs.guzzlephp.org)
+- [Documentation](docs/index.md)
 - [Stack Overflow](https://stackoverflow.com/questions/tagged/guzzle)
 - [Stack Overflow](https://stackoverflow.com/questions/tagged/guzzle)
 - [#guzzle](https://app.slack.com/client/T0D2S9JCT/CE6UAAKL4) channel on [PHP-HTTP Slack](https://slack.httplug.io/)
 - [#guzzle](https://app.slack.com/client/T0D2S9JCT/CE6UAAKL4) channel on [PHP-HTTP Slack](https://slack.httplug.io/)
 - [Gitter](https://gitter.im/guzzle/guzzle)
 - [Gitter](https://gitter.im/guzzle/guzzle)
@@ -66,17 +66,17 @@ composer require guzzlehttp/guzzle
 | 4.x     | EOL (2016-10-31)    | `guzzlehttp/guzzle` | `GuzzleHttp` | [v4][guzzle-4-repo] | N/A                 | No    | >=5.4,<7.0   |
 | 4.x     | EOL (2016-10-31)    | `guzzlehttp/guzzle` | `GuzzleHttp` | [v4][guzzle-4-repo] | N/A                 | No    | >=5.4,<7.0   |
 | 5.x     | EOL (2019-10-31)    | `guzzlehttp/guzzle` | `GuzzleHttp` | [v5][guzzle-5-repo] | [v5][guzzle-5-docs] | No    | >=5.4,<7.4   |
 | 5.x     | EOL (2019-10-31)    | `guzzlehttp/guzzle` | `GuzzleHttp` | [v5][guzzle-5-repo] | [v5][guzzle-5-docs] | No    | >=5.4,<7.4   |
 | 6.x     | EOL (2023-10-31)    | `guzzlehttp/guzzle` | `GuzzleHttp` | [v6][guzzle-6-repo] | [v6][guzzle-6-docs] | Yes   | >=5.5,<8.0   |
 | 6.x     | EOL (2023-10-31)    | `guzzlehttp/guzzle` | `GuzzleHttp` | [v6][guzzle-6-repo] | [v6][guzzle-6-docs] | Yes   | >=5.5,<8.0   |
-| 7.x     | Latest              | `guzzlehttp/guzzle` | `GuzzleHttp` | [v7][guzzle-7-repo] | [v7][guzzle-7-docs] | Yes   | >=7.2.5,<8.5 |
+| 7.x     | Latest              | `guzzlehttp/guzzle` | `GuzzleHttp` | [v7][guzzle-7-repo] | [v7][guzzle-7-docs] | Yes   | >=7.2.5,<8.7 |
 
 
 [guzzle-3-repo]: https://github.com/guzzle/guzzle3
 [guzzle-3-repo]: https://github.com/guzzle/guzzle3
 [guzzle-4-repo]: https://github.com/guzzle/guzzle/tree/4.x
 [guzzle-4-repo]: https://github.com/guzzle/guzzle/tree/4.x
 [guzzle-5-repo]: https://github.com/guzzle/guzzle/tree/5.3
 [guzzle-5-repo]: https://github.com/guzzle/guzzle/tree/5.3
 [guzzle-6-repo]: https://github.com/guzzle/guzzle/tree/6.5
 [guzzle-6-repo]: https://github.com/guzzle/guzzle/tree/6.5
-[guzzle-7-repo]: https://github.com/guzzle/guzzle
-[guzzle-3-docs]: https://guzzle3.readthedocs.io/
-[guzzle-5-docs]: https://docs.guzzlephp.org/en/5.3/
-[guzzle-6-docs]: https://docs.guzzlephp.org/en/6.5/
-[guzzle-7-docs]: https://docs.guzzlephp.org/en/latest/
+[guzzle-7-repo]: https://github.com/guzzle/guzzle/tree/7.15
+[guzzle-3-docs]: https://github.com/guzzle/guzzle3/tree/master/docs
+[guzzle-5-docs]: https://github.com/guzzle/guzzle/tree/5.3/docs
+[guzzle-6-docs]: https://github.com/guzzle/guzzle/tree/6.5/docs
+[guzzle-7-docs]: https://github.com/guzzle/guzzle/blob/7.15/docs/index.md
 
 
 
 
 ## Security
 ## Security

+ 6 - 8
modules/boonex/xero/plugins/guzzlehttp/guzzle/UPGRADING.md

@@ -12,7 +12,7 @@ Please make sure:
 - You are calling a function or a method with the correct type.
 - You are calling a function or a method with the correct type.
 - If you extend a class of Guzzle; update all signatures on methods you override.
 - If you extend a class of Guzzle; update all signatures on methods you override.
 
 
-#### Other backwards compatibility breaking changes
+#### Other Backwards Compatibility Breaking Changes
 
 
 - Class `GuzzleHttp\UriTemplate` is removed.
 - Class `GuzzleHttp\UriTemplate` is removed.
 - Class `GuzzleHttp\Exception\SeekException` is removed.
 - Class `GuzzleHttp\Exception\SeekException` is removed.
@@ -35,7 +35,7 @@ Please make sure:
 - The `log` middleware will log the errors with level `error` instead of `notice` 
 - The `log` middleware will log the errors with level `error` instead of `notice` 
 - Support for international domain names (IDN) is now disabled by default, and enabling it requires installing ext-intl, linked against a modern version of the C library (ICU 4.6 or higher).
 - Support for international domain names (IDN) is now disabled by default, and enabling it requires installing ext-intl, linked against a modern version of the C library (ICU 4.6 or higher).
 
 
-#### Native functions calls
+#### Native Functions Calls
 
 
 All internal native functions calls of Guzzle are now prefixed with a slash. This
 All internal native functions calls of Guzzle are now prefixed with a slash. This
 change makes it impossible for method overloading by other libraries or applications.
 change makes it impossible for method overloading by other libraries or applications.
@@ -49,7 +49,7 @@ curl_version();
 \curl_version();
 \curl_version();
 ```
 ```
 
 
-For the full diff you can check [here](https://github.com/guzzle/guzzle/compare/6.5.4..master).
+For the full diff you can check [here](https://github.com/guzzle/guzzle/compare/6.5.4..7.0.0).
 
 
 5.0 to 6.0
 5.0 to 6.0
 ----------
 ----------
@@ -189,11 +189,11 @@ $client = new GuzzleHttp\Client(['handler' => $handler]);
 
 
 ## POST Requests
 ## POST Requests
 
 
-This version added the [`form_params`](https://docs.guzzlephp.org/en/latest/request-options.html#form_params)
+This version added the [`form_params`](https://github.com/guzzle/guzzle/blob/6.5/docs/request-options.rst#form_params)
 and `multipart` request options. `form_params` is an associative array of
 and `multipart` request options. `form_params` is an associative array of
 strings or array of strings and is used to serialize an
 strings or array of strings and is used to serialize an
 `application/x-www-form-urlencoded` POST request. The
 `application/x-www-form-urlencoded` POST request. The
-[`multipart`](https://docs.guzzlephp.org/en/latest/request-options.html#multipart)
+[`multipart`](https://github.com/guzzle/guzzle/blob/6.5/docs/request-options.rst#multipart)
 option is now used to send a multipart/form-data POST request.
 option is now used to send a multipart/form-data POST request.
 
 
 `GuzzleHttp\Post\PostFile` has been removed. Use the `multipart` option to add
 `GuzzleHttp\Post\PostFile` has been removed. Use the `multipart` option to add
@@ -209,7 +209,7 @@ The `base_url` option has been renamed to `base_uri`.
 
 
 ## Rewritten Adapter Layer
 ## Rewritten Adapter Layer
 
 
-Guzzle now uses [RingPHP](https://ringphp.readthedocs.org/en/latest) to send
+Guzzle now uses [RingPHP](https://github.com/guzzle/RingPHP) to send
 HTTP requests. The `adapter` option in a `GuzzleHttp\Client` constructor
 HTTP requests. The `adapter` option in a `GuzzleHttp\Client` constructor
 is still supported, but it has now been renamed to `handler`. Instead of
 is still supported, but it has now been renamed to `handler`. Instead of
 passing a `GuzzleHttp\Adapter\AdapterInterface`, you must now pass a PHP
 passing a `GuzzleHttp\Adapter\AdapterInterface`, you must now pass a PHP
@@ -575,8 +575,6 @@ You can intercept a request and inject a response using the `intercept()` event
 of a `GuzzleHttp\Event\BeforeEvent`, `GuzzleHttp\Event\CompleteEvent`, and
 of a `GuzzleHttp\Event\BeforeEvent`, `GuzzleHttp\Event\CompleteEvent`, and
 `GuzzleHttp\Event\ErrorEvent` event.
 `GuzzleHttp\Event\ErrorEvent` event.
 
 
-See: https://docs.guzzlephp.org/en/latest/events.html
-
 ## Inflection
 ## Inflection
 
 
 The `Guzzle\Inflection` namespace has been removed. This is not a core concern
 The `Guzzle\Inflection` namespace has been removed. This is not a core concern

+ 46 - 44
modules/boonex/xero/plugins/guzzlehttp/guzzle/composer.json

@@ -1,6 +1,7 @@
 {
 {
     "name": "guzzlehttp/guzzle",
     "name": "guzzlehttp/guzzle",
     "description": "Guzzle is a PHP HTTP client library",
     "description": "Guzzle is a PHP HTTP client library",
+    "license": "MIT",
     "keywords": [
     "keywords": [
         "framework",
         "framework",
         "http",
         "http",
@@ -12,7 +13,6 @@
         "PSR-7",
         "PSR-7",
         "PSR-18"
         "PSR-18"
     ],
     ],
-    "license": "MIT",
     "authors": [
     "authors": [
         {
         {
             "name": "Graham Campbell",
             "name": "Graham Campbell",
@@ -50,21 +50,43 @@
             "homepage": "https://github.com/Tobion"
             "homepage": "https://github.com/Tobion"
         }
         }
     ],
     ],
+    "require": {
+        "php": "^7.2.5 || ^8.0",
+        "ext-json": "*",
+        "guzzlehttp/promises": "^2.5.3",
+        "guzzlehttp/psr7": "^2.13.1",
+        "psr/http-client": "^1.0",
+        "symfony/deprecation-contracts": "^2.5 || ^3.0",
+        "symfony/polyfill-php80": "^1.25"
+    },
+    "require-dev": {
+        "ext-curl": "*",
+        "bamarni/composer-bin-plugin": "^1.8.2",
+        "guzzle/client-integration-tests": "3.0.3",
+        "guzzlehttp/test-server": "^0.7",
+        "php-http/message-factory": "^1.1",
+        "phpunit/phpunit": "^8.5.52 || ^9.6.34",
+        "psr/log": "^1.1 || ^2.0 || ^3.0"
+    },
+    "provide": {
+        "psr/http-client-implementation": "1.0"
+    },
+    "suggest": {
+        "ext-curl": "Required for CURL handler support",
+        "ext-intl": "Required for Internationalized Domain Name (IDN) support",
+        "psr/log": "Required for using the Log middleware"
+    },
     "repositories": [
     "repositories": [
         {
         {
             "type": "package",
             "type": "package",
             "package": {
             "package": {
                 "name": "guzzle/client-integration-tests",
                 "name": "guzzle/client-integration-tests",
-                "version": "v3.0.2",
-                "dist": {
-                    "url": "https://codeload.github.com/guzzle/client-integration-tests/zip/2c025848417c1135031fdf9c728ee53d0a7ceaee",
-                    "type": "zip"
-                },
+                "version": "v3.0.3",
                 "require": {
                 "require": {
+                    "guzzlehttp/psr7": "^1.7 || ^2.0",
                     "php": "^7.2.5 || ^8.0",
                     "php": "^7.2.5 || ^8.0",
-                    "phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.3.11",
                     "php-http/message": "^1.0 || ^2.0",
                     "php-http/message": "^1.0 || ^2.0",
-                    "guzzlehttp/psr7": "^1.7 || ^2.0",
+                    "phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.3.11",
                     "th3n3rd/cartesian-product": "^0.3"
                     "th3n3rd/cartesian-product": "^0.3"
                 },
                 },
                 "autoload": {
                 "autoload": {
@@ -74,33 +96,26 @@
                 },
                 },
                 "bin": [
                 "bin": [
                     "bin/http_test_server"
                     "bin/http_test_server"
-                ]
+                ],
+                "dist": {
+                    "type": "zip",
+                    "url": "https://codeload.github.com/guzzle/client-integration-tests/zip/30edbabe49dedd95e3f21d8a25438f767b653d75"
+                }
             }
             }
         }
         }
     ],
     ],
-    "require": {
-        "php": "^7.2.5 || ^8.0",
-        "ext-json": "*",
-        "guzzlehttp/promises": "^1.5.3 || ^2.0.3",
-        "guzzlehttp/psr7": "^2.7.0",
-        "psr/http-client": "^1.0",
-        "symfony/deprecation-contracts": "^2.2 || ^3.0"
-    },
-    "provide": {
-        "psr/http-client-implementation": "1.0"
-    },
-    "require-dev": {
-        "ext-curl": "*",
-        "bamarni/composer-bin-plugin": "^1.8.2",
-        "guzzle/client-integration-tests": "3.0.2",
-        "php-http/message-factory": "^1.1",
-        "phpunit/phpunit": "^8.5.39 || ^9.6.20",
-        "psr/log": "^1.1 || ^2.0 || ^3.0"
+    "autoload": {
+        "psr-4": {
+            "GuzzleHttp\\": "src/"
+        },
+        "files": [
+            "src/functions_include.php"
+        ]
     },
     },
-    "suggest": {
-        "ext-curl": "Required for CURL handler support",
-        "ext-intl": "Required for Internationalized Domain Name (IDN) support",
-        "psr/log": "Required for using the Log middleware"
+    "autoload-dev": {
+        "psr-4": {
+            "GuzzleHttp\\Tests\\": "tests/"
+        }
     },
     },
     "config": {
     "config": {
         "allow-plugins": {
         "allow-plugins": {
@@ -114,18 +129,5 @@
             "bin-links": true,
             "bin-links": true,
             "forward-command": false
             "forward-command": false
         }
         }
-    },
-    "autoload": {
-        "psr-4": {
-            "GuzzleHttp\\": "src/"
-        },
-        "files": [
-            "src/functions_include.php"
-        ]
-    },
-    "autoload-dev": {
-        "psr-4": {
-            "GuzzleHttp\\Tests\\": "tests/"
-        }
     }
     }
 }
 }

ファイルの差分が大きいため隠しています
+ 813 - 28
modules/boonex/xero/plugins/guzzlehttp/guzzle/src/Client.php


+ 93 - 22
modules/boonex/xero/plugins/guzzlehttp/guzzle/src/Cookie/CookieJar.php

@@ -2,6 +2,7 @@
 
 
 namespace GuzzleHttp\Cookie;
 namespace GuzzleHttp\Cookie;
 
 
+use GuzzleHttp\Psr7;
 use Psr\Http\Message\RequestInterface;
 use Psr\Http\Message\RequestInterface;
 use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ResponseInterface;
 
 
@@ -10,6 +11,11 @@ use Psr\Http\Message\ResponseInterface;
  */
  */
 class CookieJar implements CookieJarInterface
 class CookieJar implements CookieJarInterface
 {
 {
+    private const MAX_SET_COOKIE_FIELD_LENGTH = 8190;
+    private const MAX_SET_COOKIE_FIELDS = 50;
+    private const MAX_REQUEST_COOKIES = 150;
+    private const MAX_COOKIE_HEADER_LENGTH = 8190;
+
     /**
     /**
      * @var SetCookie[] Loaded cookie data
      * @var SetCookie[] Loaded cookie data
      */
      */
@@ -32,7 +38,7 @@ class CookieJar implements CookieJarInterface
         $this->strictMode = $strictMode;
         $this->strictMode = $strictMode;
 
 
         foreach ($cookieArray as $cookie) {
         foreach ($cookieArray as $cookie) {
-            if (!($cookie instanceof SetCookie)) {
+            if (!$cookie instanceof SetCookie) {
                 $cookie = new SetCookie($cookie);
                 $cookie = new SetCookie($cookie);
             }
             }
             $this->setCookie($cookie);
             $this->setCookie($cookie);
@@ -88,7 +94,7 @@ class CookieJar implements CookieJarInterface
     public function getCookieByName(string $name): ?SetCookie
     public function getCookieByName(string $name): ?SetCookie
     {
     {
         foreach ($this->cookies as $cookie) {
         foreach ($this->cookies as $cookie) {
-            if ($cookie->getName() !== null && \strcasecmp($cookie->getName(), $name) === 0) {
+            if ($cookie->getName() !== null && Psr7\Utils::caselessEquals($cookie->getName(), $name)) {
                 return $cookie;
                 return $cookie;
             }
             }
         }
         }
@@ -105,22 +111,23 @@ class CookieJar implements CookieJarInterface
 
 
     public function clear(?string $domain = null, ?string $path = null, ?string $name = null): void
     public function clear(?string $domain = null, ?string $path = null, ?string $name = null): void
     {
     {
-        if (!$domain) {
+        if ($domain === null) {
             $this->cookies = [];
             $this->cookies = [];
 
 
             return;
             return;
-        } elseif (!$path) {
+        } elseif ($path === null) {
             $this->cookies = \array_filter(
             $this->cookies = \array_filter(
                 $this->cookies,
                 $this->cookies,
                 static function (SetCookie $cookie) use ($domain): bool {
                 static function (SetCookie $cookie) use ($domain): bool {
-                    return !$cookie->matchesDomain($domain);
+                    return $cookie->getDomain() === null || !$cookie->matchesDomain($domain);
                 }
                 }
             );
             );
-        } elseif (!$name) {
+        } elseif ($name === null) {
             $this->cookies = \array_filter(
             $this->cookies = \array_filter(
                 $this->cookies,
                 $this->cookies,
                 static function (SetCookie $cookie) use ($path, $domain): bool {
                 static function (SetCookie $cookie) use ($path, $domain): bool {
-                    return !($cookie->matchesPath($path)
+                    return !($cookie->getDomain() !== null
+                        && $cookie->matchesPath($path)
                         && $cookie->matchesDomain($domain));
                         && $cookie->matchesDomain($domain));
                 }
                 }
             );
             );
@@ -128,7 +135,8 @@ class CookieJar implements CookieJarInterface
             $this->cookies = \array_filter(
             $this->cookies = \array_filter(
                 $this->cookies,
                 $this->cookies,
                 static function (SetCookie $cookie) use ($path, $domain, $name) {
                 static function (SetCookie $cookie) use ($path, $domain, $name) {
-                    return !($cookie->getName() == $name
+                    return !($cookie->getDomain() !== null
+                        && $cookie->getName() === $name
                         && $cookie->matchesPath($path)
                         && $cookie->matchesPath($path)
                         && $cookie->matchesDomain($domain));
                         && $cookie->matchesDomain($domain));
                 }
                 }
@@ -166,13 +174,23 @@ class CookieJar implements CookieJarInterface
             return false;
             return false;
         }
         }
 
 
+        $maxAge = $cookie->getMaxAge();
+        if ($maxAge !== null && $maxAge <= 0) {
+            if ($cookie->getDomain() !== null) {
+                $this->removeCookie($cookie);
+            }
+
+            return false;
+        }
+
         // Resolve conflicts with previously set cookies
         // Resolve conflicts with previously set cookies
         foreach ($this->cookies as $i => $c) {
         foreach ($this->cookies as $i => $c) {
             // Two cookies are identical, when their path, and domain are
             // Two cookies are identical, when their path, and domain are
             // identical.
             // identical.
-            if ($c->getPath() != $cookie->getPath()
-                || $c->getDomain() != $cookie->getDomain()
-                || $c->getName() != $cookie->getName()
+            if ($c->getPath() !== $cookie->getPath()
+                || $c->getDomain() !== $cookie->getDomain()
+                || $c->getHostOnly() !== $cookie->getHostOnly()
+                || $c->getName() !== $cookie->getName()
             ) {
             ) {
                 continue;
                 continue;
             }
             }
@@ -222,10 +240,23 @@ class CookieJar implements CookieJarInterface
     public function extractCookies(RequestInterface $request, ResponseInterface $response): void
     public function extractCookies(RequestInterface $request, ResponseInterface $response): void
     {
     {
         if ($cookieHeader = $response->getHeader('Set-Cookie')) {
         if ($cookieHeader = $response->getHeader('Set-Cookie')) {
+            $accepted = 0;
             foreach ($cookieHeader as $cookie) {
             foreach ($cookieHeader as $cookie) {
+                if (\strlen($cookie) > self::MAX_SET_COOKIE_FIELD_LENGTH) {
+                    continue;
+                }
+
                 $sc = SetCookie::fromString($cookie);
                 $sc = SetCookie::fromString($cookie);
-                if (!$sc->getDomain()) {
+                $domain = $sc->getDomain();
+                if ($domain === null || $domain === '') {
                     $sc->setDomain($request->getUri()->getHost());
                     $sc->setDomain($request->getUri()->getHost());
+                    $sc->setHostOnly(true);
+                } elseif (\substr($domain, -1) === '.' && '' !== \trim($domain, '.')) {
+                    // Keep pure-dot domains rejected by the dot-only fix.
+                    $sc->setDomain($request->getUri()->getHost());
+                    $sc->setHostOnly(true);
+                } else {
+                    $sc->setHostOnly(false);
                 }
                 }
                 if (0 !== \strpos($sc->getPath(), '/')) {
                 if (0 !== \strpos($sc->getPath(), '/')) {
                     $sc->setPath($this->getCookiePathFromRequest($request));
                     $sc->setPath($this->getCookiePathFromRequest($request));
@@ -235,7 +266,9 @@ class CookieJar implements CookieJarInterface
                 }
                 }
                 // Note: At this point `$sc->getDomain()` being a public suffix should
                 // Note: At this point `$sc->getDomain()` being a public suffix should
                 // be rejected, but we don't want to pull in the full PSL dependency.
                 // be rejected, but we don't want to pull in the full PSL dependency.
-                $this->setCookie($sc);
+                if ($this->setCookie($sc) && ++$accepted === self::MAX_SET_COOKIE_FIELDS) {
+                    break;
+                }
             }
             }
         }
         }
     }
     }
@@ -268,19 +301,32 @@ class CookieJar implements CookieJarInterface
     public function withCookieHeader(RequestInterface $request): RequestInterface
     public function withCookieHeader(RequestInterface $request): RequestInterface
     {
     {
         $values = [];
         $values = [];
+        $headerLength = 8;
         $uri = $request->getUri();
         $uri = $request->getUri();
         $scheme = $uri->getScheme();
         $scheme = $uri->getScheme();
         $host = $uri->getHost();
         $host = $uri->getHost();
         $path = $uri->getPath() ?: '/';
         $path = $uri->getPath() ?: '/';
 
 
         foreach ($this->cookies as $cookie) {
         foreach ($this->cookies as $cookie) {
-            if ($cookie->matchesPath($path)
+            if ($cookie->getDomain() !== null
+                && $cookie->matchesPath($path)
                 && $cookie->matchesDomain($host)
                 && $cookie->matchesDomain($host)
                 && !$cookie->isExpired()
                 && !$cookie->isExpired()
                 && (!$cookie->getSecure() || $scheme === 'https')
                 && (!$cookie->getSecure() || $scheme === 'https')
             ) {
             ) {
-                $values[] = $cookie->getName().'='
-                    .$cookie->getValue();
+                $name = (string) $cookie->getName();
+                $value = (string) $cookie->getValue();
+                $separatorLength = $values === [] ? 0 : 2;
+                $valueLength = \strlen($name) + 1 + \strlen($value);
+                if ($headerLength + $separatorLength + $valueLength > self::MAX_COOKIE_HEADER_LENGTH) {
+                    break;
+                }
+
+                $values[] = $name.'='.$value;
+                $headerLength += $separatorLength + $valueLength;
+                if (\count($values) === self::MAX_REQUEST_COOKIES) {
+                    break;
+                }
             }
             }
         }
         }
 
 
@@ -296,12 +342,37 @@ class CookieJar implements CookieJarInterface
     private function removeCookieIfEmpty(SetCookie $cookie): void
     private function removeCookieIfEmpty(SetCookie $cookie): void
     {
     {
         $cookieValue = $cookie->getValue();
         $cookieValue = $cookie->getValue();
-        if ($cookieValue === null || $cookieValue === '') {
-            $this->clear(
-                $cookie->getDomain(),
-                $cookie->getPath(),
-                $cookie->getName()
-            );
+        if (($cookieValue === null || $cookieValue === '') && $cookie->getDomain() !== null) {
+            $this->removeCookie($cookie);
         }
         }
     }
     }
+
+    private function removeCookie(SetCookie $cookie): void
+    {
+        $this->cookies = \array_filter(
+            $this->cookies,
+            static function (SetCookie $stored) use ($cookie): bool {
+                return !($stored->getName() === $cookie->getName()
+                    && $stored->getPath() === $cookie->getPath()
+                    && self::cookieDomainsEqual($stored->getDomain(), $cookie->getDomain())
+                    && $stored->getHostOnly() === $cookie->getHostOnly());
+            }
+        );
+    }
+
+    private static function cookieDomainsEqual(?string $first, ?string $second): bool
+    {
+        if ($first === null || $second === null) {
+            return $first === $second;
+        }
+
+        if (isset($first[0]) && $first[0] === '.') {
+            $first = \substr($first, 1);
+        }
+        if (isset($second[0]) && $second[0] === '.') {
+            $second = \substr($second, 1);
+        }
+
+        return Psr7\Utils::caselessEquals($first, $second);
+    }
 }
 }

+ 25 - 5
modules/boonex/xero/plugins/guzzlehttp/guzzle/src/Cookie/FileCookieJar.php

@@ -2,7 +2,7 @@
 
 
 namespace GuzzleHttp\Cookie;
 namespace GuzzleHttp\Cookie;
 
 
-use GuzzleHttp\Utils;
+use GuzzleHttp\Exception\InvalidArgumentException;
 
 
 /**
 /**
  * Persists non-session cookies using a JSON formatted file
  * Persists non-session cookies using a JSON formatted file
@@ -60,11 +60,18 @@ class FileCookieJar extends CookieJar
         /** @var SetCookie $cookie */
         /** @var SetCookie $cookie */
         foreach ($this as $cookie) {
         foreach ($this as $cookie) {
             if (CookieJar::shouldPersist($cookie, $this->storeSessionCookies)) {
             if (CookieJar::shouldPersist($cookie, $this->storeSessionCookies)) {
-                $json[] = $cookie->toArray();
+                $data = $cookie->toArray();
+                $data['HostOnly'] = $cookie->getHostOnly();
+                $json[] = $data;
             }
             }
         }
         }
 
 
-        $jsonStr = Utils::jsonEncode($json);
+        $jsonStr = \json_encode($json);
+        if (\JSON_ERROR_NONE !== \json_last_error()) {
+            throw new InvalidArgumentException('json_encode error: '.\json_last_error_msg());
+        }
+
+        /** @var non-empty-string $jsonStr */
         if (false === \file_put_contents($filename, $jsonStr, \LOCK_EX)) {
         if (false === \file_put_contents($filename, $jsonStr, \LOCK_EX)) {
             throw new \RuntimeException("Unable to save file {$filename}");
             throw new \RuntimeException("Unable to save file {$filename}");
         }
         }
@@ -89,10 +96,23 @@ class FileCookieJar extends CookieJar
             return;
             return;
         }
         }
 
 
-        $data = Utils::jsonDecode($json, true);
+        $data = \json_decode($json, true);
+        if (\JSON_ERROR_NONE !== \json_last_error()) {
+            throw new InvalidArgumentException('json_decode error: '.\json_last_error_msg());
+        }
+
         if (\is_array($data)) {
         if (\is_array($data)) {
+            $cookies = [];
             foreach ($data as $cookie) {
             foreach ($data as $cookie) {
-                $this->setCookie(new SetCookie($cookie));
+                if (!\is_array($cookie) || !\array_key_exists('HostOnly', $cookie) || !\is_bool($cookie['HostOnly'])) {
+                    throw new \RuntimeException("Invalid cookie file: {$filename}");
+                }
+
+                $cookies[] = new SetCookie($cookie);
+            }
+
+            foreach ($cookies as $cookie) {
+                $this->setCookie($cookie);
             }
             }
         } elseif (\is_scalar($data) && !empty($data)) {
         } elseif (\is_scalar($data) && !empty($data)) {
             throw new \RuntimeException("Invalid cookie file: {$filename}");
             throw new \RuntimeException("Invalid cookie file: {$filename}");

+ 27 - 5
modules/boonex/xero/plugins/guzzlehttp/guzzle/src/Cookie/SessionCookieJar.php

@@ -50,11 +50,18 @@ class SessionCookieJar extends CookieJar
         /** @var SetCookie $cookie */
         /** @var SetCookie $cookie */
         foreach ($this as $cookie) {
         foreach ($this as $cookie) {
             if (CookieJar::shouldPersist($cookie, $this->storeSessionCookies)) {
             if (CookieJar::shouldPersist($cookie, $this->storeSessionCookies)) {
-                $json[] = $cookie->toArray();
+                $data = $cookie->toArray();
+                $data['HostOnly'] = $cookie->getHostOnly();
+                $json[] = $data;
             }
             }
         }
         }
 
 
-        $_SESSION[$this->sessionKey] = \json_encode($json);
+        $json = \json_encode($json);
+        if (false === $json) {
+            throw new \RuntimeException('Unable to encode cookie data');
+        }
+
+        $_SESSION[$this->sessionKey] = $json;
     }
     }
 
 
     /**
     /**
@@ -65,12 +72,27 @@ class SessionCookieJar extends CookieJar
         if (!isset($_SESSION[$this->sessionKey])) {
         if (!isset($_SESSION[$this->sessionKey])) {
             return;
             return;
         }
         }
-        $data = \json_decode($_SESSION[$this->sessionKey], true);
+
+        $json = $_SESSION[$this->sessionKey];
+        if (!\is_string($json)) {
+            throw new \RuntimeException('Invalid cookie data');
+        }
+
+        $data = \json_decode($json, true);
         if (\is_array($data)) {
         if (\is_array($data)) {
+            $cookies = [];
             foreach ($data as $cookie) {
             foreach ($data as $cookie) {
-                $this->setCookie(new SetCookie($cookie));
+                if (!\is_array($cookie) || !\array_key_exists('HostOnly', $cookie) || !\is_bool($cookie['HostOnly'])) {
+                    throw new \RuntimeException('Invalid cookie data');
+                }
+
+                $cookies[] = new SetCookie($cookie);
+            }
+
+            foreach ($cookies as $cookie) {
+                $this->setCookie($cookie);
             }
             }
-        } elseif (\strlen($data)) {
+        } elseif (\is_scalar($data) && \strlen((string) $data)) {
             throw new \RuntimeException('Invalid cookie data');
             throw new \RuntimeException('Invalid cookie data');
         }
         }
     }
     }

+ 150 - 31
modules/boonex/xero/plugins/guzzlehttp/guzzle/src/Cookie/SetCookie.php

@@ -2,6 +2,9 @@
 
 
 namespace GuzzleHttp\Cookie;
 namespace GuzzleHttp\Cookie;
 
 
+use GuzzleHttp\Handler\HostValidator;
+use GuzzleHttp\Psr7;
+
 /**
 /**
  * Set-Cookie object
  * Set-Cookie object
  */
  */
@@ -27,6 +30,11 @@ class SetCookie
      */
      */
     private $data;
     private $data;
 
 
+    /**
+     * @var bool Whether this cookie was set without a Domain attribute
+     */
+    private $hostOnly = false;
+
     /**
     /**
      * Create a new SetCookie object from a string.
      * Create a new SetCookie object from a string.
      *
      *
@@ -37,7 +45,9 @@ class SetCookie
         // Create the default return array
         // Create the default return array
         $data = self::$defaults;
         $data = self::$defaults;
         // Explode the cookie string using a series of semicolons
         // Explode the cookie string using a series of semicolons
-        $pieces = \array_filter(\array_map('trim', \explode(';', $cookie)));
+        $pieces = \array_filter(\array_map(static function (string $piece): string {
+            return \trim($piece, " \n\r\t\0\x0B");
+        }, \explode(';', $cookie)));
         // The name of the cookie (first kvp) must exist and include an equal sign.
         // The name of the cookie (first kvp) must exist and include an equal sign.
         if (!isset($pieces[0]) || \strpos($pieces[0], '=') === false) {
         if (!isset($pieces[0]) || \strpos($pieces[0], '=') === false) {
             return new self($data);
             return new self($data);
@@ -46,7 +56,7 @@ class SetCookie
         // Add the cookie pieces into the parsed data array
         // Add the cookie pieces into the parsed data array
         foreach ($pieces as $part) {
         foreach ($pieces as $part) {
             $cookieParts = \explode('=', $part, 2);
             $cookieParts = \explode('=', $part, 2);
-            $key = \trim($cookieParts[0]);
+            $key = \trim($cookieParts[0], " \n\r\t\0\x0B");
             $value = isset($cookieParts[1])
             $value = isset($cookieParts[1])
                 ? \trim($cookieParts[1], " \n\r\t\0\x0B")
                 ? \trim($cookieParts[1], " \n\r\t\0\x0B")
                 : true;
                 : true;
@@ -57,7 +67,7 @@ class SetCookie
                 $data['Value'] = $value;
                 $data['Value'] = $value;
             } else {
             } else {
                 foreach (\array_keys(self::$defaults) as $search) {
                 foreach (\array_keys(self::$defaults) as $search) {
-                    if (!\strcasecmp($search, $key)) {
+                    if (Psr7\Utils::caselessEquals($search, $key)) {
                         if ($search === 'Max-Age') {
                         if ($search === 'Max-Age') {
                             if (is_numeric($value)) {
                             if (is_numeric($value)) {
                                 $data[$search] = (int) $value;
                                 $data[$search] = (int) $value;
@@ -72,6 +82,9 @@ class SetCookie
                         continue 2;
                         continue 2;
                     }
                     }
                 }
                 }
+                if (Psr7\Utils::caselessEquals('HostOnly', $key)) {
+                    continue;
+                }
                 $data[$key] = $value;
                 $data[$key] = $value;
             }
             }
         }
         }
@@ -86,6 +99,14 @@ class SetCookie
     {
     {
         $this->data = self::$defaults;
         $this->data = self::$defaults;
 
 
+        if (\array_key_exists('HostOnly', $data)) {
+            if (!\is_bool($data['HostOnly'])) {
+                throw new \InvalidArgumentException('Cookie field "HostOnly" must be a boolean');
+            }
+            $this->setHostOnly($data['HostOnly']);
+            unset($data['HostOnly']);
+        }
+
         if (isset($data['Name'])) {
         if (isset($data['Name'])) {
             $this->setName($data['Name']);
             $this->setName($data['Name']);
         }
         }
@@ -128,18 +149,34 @@ class SetCookie
         }
         }
 
 
         // Extract the Expires value and turn it into a UNIX timestamp if needed
         // Extract the Expires value and turn it into a UNIX timestamp if needed
-        if (!$this->getExpires() && $this->getMaxAge()) {
+        $maxAge = $this->getMaxAge();
+        if (!$this->getExpires() && $maxAge !== null) {
             // Calculate the Expires date
             // Calculate the Expires date
-            $this->setExpires(\time() + $this->getMaxAge());
+            $this->setExpires(self::maxAgeToExpires($maxAge, \time()));
         } elseif (null !== ($expires = $this->getExpires()) && !\is_numeric($expires)) {
         } elseif (null !== ($expires = $this->getExpires()) && !\is_numeric($expires)) {
             $this->setExpires($expires);
             $this->setExpires($expires);
         }
         }
     }
     }
 
 
+    private static function maxAgeToExpires(int $maxAge, int $now): int
+    {
+        if ($maxAge <= 0) {
+            return $now - 1;
+        }
+        if ($maxAge > \PHP_INT_MAX - $now) {
+            return \PHP_INT_MAX;
+        }
+
+        return $now + $maxAge;
+    }
+
     public function __toString()
     public function __toString()
     {
     {
         $str = $this->data['Name'].'='.($this->data['Value'] ?? '').'; ';
         $str = $this->data['Name'].'='.($this->data['Value'] ?? '').'; ';
         foreach ($this->data as $k => $v) {
         foreach ($this->data as $k => $v) {
+            if ($k === 'Domain' && $this->getHostOnly()) {
+                continue;
+            }
             if ($k !== 'Name' && $k !== 'Value' && $v !== null && $v !== false) {
             if ($k !== 'Name' && $k !== 'Value' && $v !== null && $v !== false) {
                 if ($k === 'Expires') {
                 if ($k === 'Expires') {
                     $str .= 'Expires='.\gmdate('D, d M Y H:i:s \G\M\T', $v).'; ';
                     $str .= 'Expires='.\gmdate('D, d M Y H:i:s \G\M\T', $v).'; ';
@@ -154,7 +191,12 @@ class SetCookie
 
 
     public function toArray(): array
     public function toArray(): array
     {
     {
-        return $this->data;
+        $data = $this->data;
+        if ($this->getHostOnly()) {
+            $data['HostOnly'] = true;
+        }
+
+        return $data;
     }
     }
 
 
     /**
     /**
@@ -175,7 +217,7 @@ class SetCookie
     public function setName($name): void
     public function setName($name): void
     {
     {
         if (!is_string($name)) {
         if (!is_string($name)) {
-            trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a string to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
+            \trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a string to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
         }
         }
 
 
         $this->data['Name'] = (string) $name;
         $this->data['Name'] = (string) $name;
@@ -199,7 +241,7 @@ class SetCookie
     public function setValue($value): void
     public function setValue($value): void
     {
     {
         if (!is_string($value)) {
         if (!is_string($value)) {
-            trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a string to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
+            \trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a string to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
         }
         }
 
 
         $this->data['Value'] = (string) $value;
         $this->data['Value'] = (string) $value;
@@ -223,12 +265,32 @@ class SetCookie
     public function setDomain($domain): void
     public function setDomain($domain): void
     {
     {
         if (!is_string($domain) && null !== $domain) {
         if (!is_string($domain) && null !== $domain) {
-            trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a string or null to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
+            \trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a string or null to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
         }
         }
 
 
         $this->data['Domain'] = null === $domain ? null : (string) $domain;
         $this->data['Domain'] = null === $domain ? null : (string) $domain;
     }
     }
 
 
+    /**
+     * Get whether this cookie is scoped to the origin host only.
+     *
+     * @return bool
+     */
+    public function getHostOnly()
+    {
+        return $this->hostOnly;
+    }
+
+    /**
+     * Set whether this cookie is scoped to the origin host only.
+     *
+     * @param bool $hostOnly Set to true for host-only cookies
+     */
+    public function setHostOnly(bool $hostOnly): void
+    {
+        $this->hostOnly = $hostOnly;
+    }
+
     /**
     /**
      * Get the path.
      * Get the path.
      *
      *
@@ -247,7 +309,7 @@ class SetCookie
     public function setPath($path): void
     public function setPath($path): void
     {
     {
         if (!is_string($path)) {
         if (!is_string($path)) {
-            trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a string to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
+            \trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a string to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
         }
         }
 
 
         $this->data['Path'] = (string) $path;
         $this->data['Path'] = (string) $path;
@@ -271,7 +333,7 @@ class SetCookie
     public function setMaxAge($maxAge): void
     public function setMaxAge($maxAge): void
     {
     {
         if (!is_int($maxAge) && null !== $maxAge) {
         if (!is_int($maxAge) && null !== $maxAge) {
-            trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing an int or null to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
+            \trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing an int or null to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
         }
         }
 
 
         $this->data['Max-Age'] = $maxAge === null ? null : (int) $maxAge;
         $this->data['Max-Age'] = $maxAge === null ? null : (int) $maxAge;
@@ -295,10 +357,18 @@ class SetCookie
     public function setExpires($timestamp): void
     public function setExpires($timestamp): void
     {
     {
         if (!is_int($timestamp) && !is_string($timestamp) && null !== $timestamp) {
         if (!is_int($timestamp) && !is_string($timestamp) && null !== $timestamp) {
-            trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing an int, string or null to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
+            \trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing an int, string or null to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
         }
         }
 
 
-        $this->data['Expires'] = null === $timestamp ? null : (\is_numeric($timestamp) ? (int) $timestamp : \strtotime((string) $timestamp));
+        if (null === $timestamp) {
+            $this->data['Expires'] = null;
+        } elseif (\is_numeric($timestamp)) {
+            $this->data['Expires'] = (int) $timestamp;
+        } else {
+            // Store unparseable dates as session cookies, not as expired cookies.
+            $expires = \strtotime((string) $timestamp);
+            $this->data['Expires'] = $expires === false ? null : $expires;
+        }
     }
     }
 
 
     /**
     /**
@@ -319,7 +389,7 @@ class SetCookie
     public function setSecure($secure): void
     public function setSecure($secure): void
     {
     {
         if (!is_bool($secure)) {
         if (!is_bool($secure)) {
-            trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a bool to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
+            \trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a bool to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
         }
         }
 
 
         $this->data['Secure'] = (bool) $secure;
         $this->data['Secure'] = (bool) $secure;
@@ -343,7 +413,7 @@ class SetCookie
     public function setDiscard($discard): void
     public function setDiscard($discard): void
     {
     {
         if (!is_bool($discard)) {
         if (!is_bool($discard)) {
-            trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a bool to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
+            \trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a bool to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
         }
         }
 
 
         $this->data['Discard'] = (bool) $discard;
         $this->data['Discard'] = (bool) $discard;
@@ -367,7 +437,7 @@ class SetCookie
     public function setHttpOnly($httpOnly): void
     public function setHttpOnly($httpOnly): void
     {
     {
         if (!is_bool($httpOnly)) {
         if (!is_bool($httpOnly)) {
-            trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a bool to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
+            \trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a bool to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
         }
         }
 
 
         $this->data['HttpOnly'] = (bool) $httpOnly;
         $this->data['HttpOnly'] = (bool) $httpOnly;
@@ -393,7 +463,7 @@ class SetCookie
         $cookiePath = $this->getPath();
         $cookiePath = $this->getPath();
 
 
         // Match on exact matches or when path is the default empty "/"
         // Match on exact matches or when path is the default empty "/"
-        if ($cookiePath === '/' || $cookiePath == $requestPath) {
+        if ($cookiePath === '/' || $cookiePath === $requestPath) {
             return true;
             return true;
         }
         }
 
 
@@ -420,27 +490,79 @@ class SetCookie
     {
     {
         $cookieDomain = $this->getDomain();
         $cookieDomain = $this->getDomain();
         if (null === $cookieDomain) {
         if (null === $cookieDomain) {
-            return true;
+            return !$this->getHostOnly();
+        }
+
+        if ($this->getHostOnly()) {
+            return Psr7\Utils::asciiToLower($domain) === Psr7\Utils::asciiToLower($cookieDomain);
         }
         }
 
 
         // Remove the leading '.' as per spec in RFC 6265.
         // Remove the leading '.' as per spec in RFC 6265.
         // https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.3
         // https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.3
-        $cookieDomain = \ltrim(\strtolower($cookieDomain), '.');
-
-        $domain = \strtolower($domain);
+        $cookieDomain = Psr7\Utils::asciiToLower($cookieDomain);
+        if ($cookieDomain !== '' && $cookieDomain[0] === '.') {
+            /** @var string */
+            $cookieDomain = \substr($cookieDomain, 1);
+        }
+        if ('' === $cookieDomain) {
+            return false;
+        }
 
 
-        // Domain not set or exact match.
-        if ('' === $cookieDomain || $domain === $cookieDomain) {
+        $domain = Psr7\Utils::asciiToLower($domain);
+        if ($domain === $cookieDomain) {
             return true;
             return true;
         }
         }
 
 
+        // A percent-escaped cookie domain can decode to another host spelling.
+        // Keep it exact-match-only to avoid extending that host's cookie scope.
+        if (\strpos($cookieDomain, '%') !== false) {
+            return false;
+        }
+
+        // IP literals and numeric hosts are exact-match-only per RFC 6265.
+        // Only the exact match above may succeed for those cookie domains.
+        if (self::isIpAddressOrNumericHost($cookieDomain)) {
+            return false;
+        }
+
         // Matching the subdomain according to RFC 6265.
         // Matching the subdomain according to RFC 6265.
         // https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.3
         // https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.3
         if (\filter_var($domain, \FILTER_VALIDATE_IP)) {
         if (\filter_var($domain, \FILTER_VALIDATE_IP)) {
             return false;
             return false;
         }
         }
 
 
-        return (bool) \preg_match('/\.'.\preg_quote($cookieDomain, '/').'$/', $domain);
+        return (bool) \preg_match('/\.'.\preg_quote($cookieDomain, '/').'$/D', $domain);
+    }
+
+    private static function isIpAddressOrNumericHost(string $host): bool
+    {
+        // Strip one root dot before detection so trailing-dot numeric hosts
+        // still cannot be matched by subdomains.
+        if ($host !== '' && \str_ends_with($host, '.')) {
+            $host = \substr($host, 0, -1);
+        }
+
+        if (\str_starts_with($host, '[') && \str_ends_with($host, ']')) {
+            $host = \substr($host, 1, -1);
+        }
+
+        if (\filter_var($host, \FILTER_VALIDATE_IP) !== false) {
+            return true;
+        }
+
+        // Public DNS names do not have an all-numeric rightmost label; treat
+        // those private/internal hosts as exact-match-only too.
+        $labels = \explode('.', $host);
+        $last = (string) \end($labels);
+
+        if ($last !== '' && \ctype_digit($last)) {
+            return true;
+        }
+
+        // Apply the transport's decimal, octal and hexadecimal inet_aton-style
+        // grammar. Omitting range checks conservatively holds some names to an
+        // exact match.
+        return HostValidator::isNumericIpv4Host(\rtrim($host, '.'));
     }
     }
 
 
     /**
     /**
@@ -464,10 +586,7 @@ class SetCookie
         }
         }
 
 
         // Check if any of the invalid characters are present in the cookie name
         // Check if any of the invalid characters are present in the cookie name
-        if (\preg_match(
-            '/[\x00-\x20\x22\x28-\x29\x2c\x2f\x3a-\x40\x5c\x7b\x7d\x7f]/',
-            $name
-        )) {
+        if (\preg_match('/[\x00-\x20\x22\x28-\x29\x2c\x2f\x3a-\x40\x5c\x7b\x7d\x7f]/', $name) !== 0) {
             return 'Cookie name must not contain invalid characters: ASCII '
             return 'Cookie name must not contain invalid characters: ASCII '
                 .'Control characters (0-31;127), space, tab and the '
                 .'Control characters (0-31;127), space, tab and the '
                 .'following characters: ()<>@,;:\"/?={}';
                 .'following characters: ()<>@,;:\"/?={}';
@@ -480,10 +599,10 @@ class SetCookie
             return 'The cookie value must not be empty';
             return 'The cookie value must not be empty';
         }
         }
 
 
-        // Domains must not be empty, but can be 0. "0" is not a valid internet
-        // domain, but may be used as server name in a private network.
+        // Domains must not be empty, but may be omitted. "0" is not a valid
+        // internet domain, but may be used as server name in a private network.
         $domain = $this->getDomain();
         $domain = $this->getDomain();
-        if ($domain === null || $domain === '') {
+        if ($domain === '' || (null !== $domain && '' === \ltrim(\trim($domain, " \n\r\t\0\x0B"), '.'))) {
             return 'The cookie domain must not be empty';
             return 'The cookie domain must not be empty';
         }
         }
 
 

+ 0 - 2
modules/boonex/xero/plugins/guzzlehttp/guzzle/src/Exception/ConnectException.php

@@ -7,8 +7,6 @@ use Psr\Http\Message\RequestInterface;
 
 
 /**
 /**
  * Exception thrown when a connection cannot be established.
  * Exception thrown when a connection cannot be established.
- *
- * Note that no response is present for a ConnectException
  */
  */
 class ConnectException extends TransferException implements NetworkExceptionInterface
 class ConnectException extends TransferException implements NetworkExceptionInterface
 {
 {

+ 4 - 0
modules/boonex/xero/plugins/guzzlehttp/guzzle/src/Exception/RequestException.php

@@ -45,9 +45,13 @@ class RequestException extends TransferException implements RequestExceptionInte
 
 
     /**
     /**
      * Wrap non-RequestExceptions with a RequestException
      * Wrap non-RequestExceptions with a RequestException
+     *
+     * @deprecated since 7.11. Create a RequestException directly instead.
      */
      */
     public static function wrapException(RequestInterface $request, \Throwable $e): RequestException
     public static function wrapException(RequestInterface $request, \Throwable $e): RequestException
     {
     {
+        \trigger_deprecation('guzzlehttp/guzzle', '7.11', '%s::wrapException() is deprecated and will be removed in 8.0. Create a %s directly instead.', self::class, self::class);
+
         return $e instanceof RequestException ? $e : new RequestException($e->getMessage(), $request, null, $e);
         return $e instanceof RequestException ? $e : new RequestException($e->getMessage(), $request, null, $e);
     }
     }
 
 

ファイルの差分が大きいため隠しています
+ 1738 - 143
modules/boonex/xero/plugins/guzzlehttp/guzzle/src/Handler/CurlFactory.php


+ 44 - 3
modules/boonex/xero/plugins/guzzlehttp/guzzle/src/Handler/CurlHandler.php

@@ -3,6 +3,7 @@
 namespace GuzzleHttp\Handler;
 namespace GuzzleHttp\Handler;
 
 
 use GuzzleHttp\Promise\PromiseInterface;
 use GuzzleHttp\Promise\PromiseInterface;
+use GuzzleHttp\TransportSharing;
 use Psr\Http\Message\RequestInterface;
 use Psr\Http\Message\RequestInterface;
 
 
 /**
 /**
@@ -16,30 +17,70 @@ use Psr\Http\Message\RequestInterface;
  */
  */
 class CurlHandler
 class CurlHandler
 {
 {
+    private const KNOWN_CONSTRUCTOR_OPTIONS = [
+        'handle_factory' => true,
+        'transport_sharing' => true,
+    ];
+
     /**
     /**
      * @var CurlFactoryInterface
      * @var CurlFactoryInterface
      */
      */
     private $factory;
     private $factory;
 
 
+    /**
+     * @var CurlShareHandleState|null
+     */
+    private $shareHandleState;
+
     /**
     /**
      * Accepts an associative array of options:
      * Accepts an associative array of options:
      *
      *
      * - handle_factory: Optional curl factory used to create cURL handles.
      * - handle_factory: Optional curl factory used to create cURL handles.
+     * - transport_sharing: Optional transport sharing mode.
      *
      *
-     * @param array{handle_factory?: ?CurlFactoryInterface} $options Array of options to use with the handler
+     * @param array{handle_factory?: ?CurlFactoryInterface, transport_sharing?: mixed} $options Array of options to use with the handler
      */
      */
     public function __construct(array $options = [])
     public function __construct(array $options = [])
     {
     {
-        $this->factory = $options['handle_factory']
-            ?? new CurlFactory(3);
+        foreach ($options as $name => $_) {
+            if (!isset(self::KNOWN_CONSTRUCTOR_OPTIONS[$name])) {
+                \trigger_deprecation('guzzlehttp/guzzle', '7.14', \sprintf('The "%s" CurlHandler constructor option is unknown; guzzlehttp/guzzle 8.0 will reject unknown constructor options.', (string) $name));
+            }
+        }
+
+        CurlShareHandleState::assertNoRequiredSharingCustomFactoryConflict($options, 'CurlHandler');
+        $transportSharing = $options['transport_sharing'] ?? null;
+        $sharingMode = CurlShareHandleState::normalizeMode($transportSharing, 'transport_sharing');
+
+        if (\array_key_exists('handle_factory', $options) && $options['handle_factory'] !== null) {
+            $this->shareHandleState = null;
+            $this->factory = $options['handle_factory'];
+
+            return;
+        }
+
+        $this->shareHandleState = $sharingMode !== TransportSharing::NONE
+            ? CurlShareHandleState::fromOption($transportSharing)
+            : null;
+
+        $this->factory = $this->shareHandleState !== null
+            ? new CurlFactory(3, $this->shareHandleState->mode, $this->shareHandleState)
+            : new CurlFactory(3);
     }
     }
 
 
     public function __invoke(RequestInterface $request, array $options): PromiseInterface
     public function __invoke(RequestInterface $request, array $options): PromiseInterface
     {
     {
+        HostValidator::assertRequestHost($request);
+
         if (isset($options['delay'])) {
         if (isset($options['delay'])) {
             \usleep($options['delay'] * 1000);
             \usleep($options['delay'] * 1000);
         }
         }
 
 
+        // A Multiplexing::NONE request option holds unconditionally here:
+        // transport sharing never shares the connection cache on this
+        // branch, and nothing else executes during the blocking curl_exec(),
+        // so the transfer cannot share its connection with a concurrent
+        // transfer.
         $easy = $this->factory->create($request, $options);
         $easy = $this->factory->create($request, $options);
         \curl_exec($easy->handle);
         \curl_exec($easy->handle);
         $easy->errno = \curl_errno($easy->handle);
         $easy->errno = \curl_errno($easy->handle);

+ 1232 - 64
modules/boonex/xero/plugins/guzzlehttp/guzzle/src/Handler/CurlMultiHandler.php

@@ -3,9 +3,13 @@
 namespace GuzzleHttp\Handler;
 namespace GuzzleHttp\Handler;
 
 
 use Closure;
 use Closure;
+use GuzzleHttp\Exception\RequestException;
+use GuzzleHttp\Multiplexing;
 use GuzzleHttp\Promise as P;
 use GuzzleHttp\Promise as P;
 use GuzzleHttp\Promise\Promise;
 use GuzzleHttp\Promise\Promise;
 use GuzzleHttp\Promise\PromiseInterface;
 use GuzzleHttp\Promise\PromiseInterface;
+use GuzzleHttp\Psr7;
+use GuzzleHttp\TransportSharing;
 use GuzzleHttp\Utils;
 use GuzzleHttp\Utils;
 use Psr\Http\Message\RequestInterface;
 use Psr\Http\Message\RequestInterface;
 
 
@@ -20,11 +24,41 @@ use Psr\Http\Message\RequestInterface;
  */
  */
 class CurlMultiHandler
 class CurlMultiHandler
 {
 {
+    private const KNOWN_CONSTRUCTOR_OPTIONS = [
+        'handle_factory' => true,
+        'max_host_connections' => true,
+        'max_total_connections' => true,
+        'multiplex' => true,
+        'options' => true,
+        'select_timeout' => true,
+        'transport_sharing' => true,
+    ];
+
+    private const CONNECTION_CAP_OPTIONS = [
+        'max_host_connections' => 'CURLMOPT_MAX_HOST_CONNECTIONS',
+        'max_total_connections' => 'CURLMOPT_MAX_TOTAL_CONNECTIONS',
+    ];
+
+    /**
+     * cURL options that isolate a transfer from foreign proxy tunnel
+     * connections. Failing to apply either one would fall open into
+     * credential-bearing connection reuse.
+     */
+    private const PROXY_TUNNEL_ISOLATION_OPTIONS = [
+        'CURLOPT_FRESH_CONNECT',
+        'CURLOPT_FORBID_REUSE',
+    ];
+
     /**
     /**
      * @var CurlFactoryInterface
      * @var CurlFactoryInterface
      */
      */
     private $factory;
     private $factory;
 
 
+    /**
+     * @var CurlShareHandleState|null
+     */
+    private $shareHandleState;
+
     /**
     /**
      * @var int
      * @var int
      */
      */
@@ -54,32 +88,190 @@ class CurlMultiHandler
      */
      */
     private $options = [];
     private $options = [];
 
 
+    /**
+     * @var array<int, true> Native options derived from first-class
+     *                       constructor options; failing to apply one is an
+     *                       error rather than a compatibility warning.
+     */
+    private $requiredOptions = [];
+
+    /**
+     * @var bool Whether any connection cap constructor option was applied
+     */
+    private $connectionCapsApplied = false;
+
+    /**
+     * @var bool Whether the "multiplex" constructor option disabled
+     *           multiplexing on this handler's multi handle
+     */
+    private $multiplexDisabled = false;
+
+    /**
+     * @var bool Whether a custom "handle_factory" constructor option supplies
+     *           the easy handles
+     */
+    private $customHandleFactory = false;
+
     /** @var resource|\CurlMultiHandle */
     /** @var resource|\CurlMultiHandle */
     private $_mh;
     private $_mh;
 
 
+    /**
+     * @var int Depth of nested guarded native operations (execution and
+     *          handle removal, both of which can run user callbacks). A
+     *          callback can re-enter tick(), and the nested frame must not
+     *          clear the outer frame's guard; deferred work stays parked
+     *          until the outermost frame unwinds.
+     */
+    private $multiExecDepth = 0;
+
+    /**
+     * @var bool Guards finishDeferredWork() against re-entry from the
+     *           guarded native removals it performs while flushing.
+     */
+    private $finishingDeferredWork = false;
+
+    /**
+     * @var array<int, array{easy: EasyHandle, attached: bool}>
+     */
+    private $deferredCancels = [];
+
+    /**
+     * @var array<int, object|null> Wait tokens of requests created from inside
+     *                              a cURL callback, keyed by handle id; native
+     *                              attachment is deferred until the outermost
+     *                              native execution unwinds.
+     */
+    private $deferredAdds = [];
+
+    /**
+     * @var string|null Owner signature of the proxy tunnels the multi handle's
+     *                  connection cache may hold
+     */
+    private $proxyTunnelOwner;
+
+    /** @var array<string, int> Count of attached transfers per proxy tunnel signature. */
+    private $activeProxyTunnelSignatures = [];
+
+    /** @var array<int, string> Maps an attached handle id to its proxy tunnel signature. */
+    private $activeProxyTunnelHandles = [];
+
+    /**
+     * @var int Depth of nested processMessages() calls. Guards against
+     *          multi-handle recreation re-entrancy from processMessages (a
+     *          retried transfer re-invokes the handler); a depth is tracked
+     *          because a completion callback can re-enter tick().
+     */
+    private $messageProcessingDepth = 0;
+
     /**
     /**
      * This handler accepts the following options:
      * This handler accepts the following options:
      *
      *
      * - handle_factory: An optional factory  used to create curl handles
      * - handle_factory: An optional factory  used to create curl handles
+     * - transport_sharing: Optional transport sharing mode.
      * - select_timeout: Optional timeout (in seconds) to block before timing
      * - select_timeout: Optional timeout (in seconds) to block before timing
      *   out while selecting curl handles. Defaults to 1 second.
      *   out while selecting curl handles. Defaults to 1 second.
+     * - max_host_connections: Optional maximum concurrent connections per host.
+     * - max_total_connections: Optional maximum concurrent connections overall.
+     * - multiplex: Optional Multiplexing::NONE to disallow multiplexing on
+     *   this handler's multi handle. The eager, wait, and required modes are
+     *   request options, not handler options; Multiplexing::NONE is also
+     *   conditionally accepted as a request option value.
      * - options: An associative array of CURLMOPT_* options and
      * - options: An associative array of CURLMOPT_* options and
      *   corresponding values for curl_multi_setopt()
      *   corresponding values for curl_multi_setopt()
      */
      */
     public function __construct(array $options = [])
     public function __construct(array $options = [])
     {
     {
-        $this->factory = $options['handle_factory'] ?? new CurlFactory(50);
+        foreach ($options as $name => $_) {
+            if (!isset(self::KNOWN_CONSTRUCTOR_OPTIONS[$name])) {
+                \trigger_deprecation('guzzlehttp/guzzle', '7.14', \sprintf('The "%s" CurlMultiHandler constructor option is unknown; guzzlehttp/guzzle 8.0 will reject unknown constructor options.', (string) $name));
+            }
+        }
+
+        $handlerMultiplex = $options['multiplex'] ?? null;
+        if (null !== $handlerMultiplex && Multiplexing::NONE !== $handlerMultiplex) {
+            if (\in_array($handlerMultiplex, [Multiplexing::EAGER, Multiplexing::WAIT, Multiplexing::REQUIRE_EAGER, Multiplexing::REQUIRE_WAIT], true)) {
+                throw new \InvalidArgumentException('The "multiplex" CurlMultiHandler option only accepts Multiplexing::NONE; the eager, wait, and required modes are request options.');
+            }
+
+            throw new \InvalidArgumentException(\sprintf('The "multiplex" CurlMultiHandler option must be null or Multiplexing::NONE; received %s.', \get_debug_type($handlerMultiplex)));
+        }
+        $this->multiplexDisabled = null !== $handlerMultiplex;
+
+        if ($this->multiplexDisabled && !\defined('CURLMOPT_PIPELINING')) {
+            // ext-curl only defines the constant when built against libcurl
+            // 7.16 or newer headers, and such builds compile out the matching
+            // curl_multi_setopt() case, so the guarantee cannot be applied.
+            throw new \InvalidArgumentException('The "multiplex" CurlMultiHandler option requires CURLMOPT_PIPELINING, but it is not available in the installed PHP cURL extension.');
+        }
+
+        CurlShareHandleState::assertNoRequiredSharingCustomFactoryConflict($options, 'CurlMultiHandler');
+        $transportSharing = $options['transport_sharing'] ?? null;
+        $sharingMode = CurlShareHandleState::normalizeMode($transportSharing, 'transport_sharing');
+
+        if (\array_key_exists('handle_factory', $options) && $options['handle_factory'] !== null) {
+            $this->shareHandleState = null;
+            $this->factory = $options['handle_factory'];
+            $this->customHandleFactory = true;
+        } else {
+            $this->shareHandleState = $sharingMode !== TransportSharing::NONE
+                ? CurlShareHandleState::fromOption($transportSharing)
+                : null;
+
+            $this->factory = $this->shareHandleState !== null
+                ? new CurlFactory(50, $this->shareHandleState->mode, $this->shareHandleState)
+                : new CurlFactory(50);
+        }
 
 
         if (isset($options['select_timeout'])) {
         if (isset($options['select_timeout'])) {
-            $this->selectTimeout = $options['select_timeout'];
+            $selectTimeout = $options['select_timeout'];
+            if (!\is_int($selectTimeout) && !\is_float($selectTimeout) && (!\is_string($selectTimeout) || !\is_numeric($selectTimeout))) {
+                \trigger_deprecation('guzzlehttp/guzzle', '7.14', 'Passing a non-numeric "select_timeout" CurlMultiHandler option is deprecated; guzzlehttp/guzzle 8.0 will reject it.');
+            } else {
+                $seconds = (float) $selectTimeout;
+                if (!\is_finite($seconds) || $seconds < 0 || ($seconds > 0 && (int) ($seconds * 1000) === 0)) {
+                    \trigger_deprecation('guzzlehttp/guzzle', '7.14', 'Passing a "select_timeout" CurlMultiHandler option that is not 0 or greater than or equal to 0.001 seconds is deprecated; guzzlehttp/guzzle 8.0 will reject it.');
+                }
+            }
+
+            $this->selectTimeout = $selectTimeout;
         } elseif ($selectTimeout = Utils::getenv('GUZZLE_CURL_SELECT_TIMEOUT')) {
         } elseif ($selectTimeout = Utils::getenv('GUZZLE_CURL_SELECT_TIMEOUT')) {
-            @trigger_error('Since guzzlehttp/guzzle 7.2.0: Using environment variable GUZZLE_CURL_SELECT_TIMEOUT is deprecated. Use option "select_timeout" instead.', \E_USER_DEPRECATED);
+            \trigger_deprecation('guzzlehttp/guzzle', '7.2', 'The GUZZLE_CURL_SELECT_TIMEOUT environment variable is deprecated; use the "select_timeout" option instead.');
             $this->selectTimeout = (int) $selectTimeout;
             $this->selectTimeout = (int) $selectTimeout;
         } else {
         } else {
             $this->selectTimeout = 1;
             $this->selectTimeout = 1;
         }
         }
 
 
-        $this->options = $options['options'] ?? [];
+        $multiOptions = $options['options'] ?? [];
+        if (\is_array($multiOptions)) {
+            self::rejectConnectionCapOptionConflicts($options, $multiOptions);
+
+            if ($this->multiplexDisabled && \array_key_exists(\CURLMOPT_PIPELINING, $multiOptions)) {
+                // Key presence alone conflicts, even with an agreeing value:
+                // the named option is the single multiplexing authority.
+                throw new \InvalidArgumentException('multiplex conflicts with a CURLMOPT_PIPELINING entry in the "options" array.');
+            }
+
+            self::triggerConflictingCurlMultiOptionDeprecations($multiOptions);
+        } elseif (self::hasConnectionCapOption($options)) {
+            throw new \InvalidArgumentException('options must be an array of cURL multi options when using connection cap options.');
+        } elseif ($this->multiplexDisabled) {
+            throw new \InvalidArgumentException('options must be an array of cURL multi options when using the "multiplex" option.');
+        }
+
+        $this->options = $multiOptions;
+
+        if (\is_array($multiOptions)) {
+            $this->addConnectionCapOptions($options);
+
+            if ($this->multiplexDisabled) {
+                // CURLPIPE_NOTHING; the constant itself needs libcurl 7.43
+                // headers, newer than the oldest supported runtimes. The
+                // option is required: a handler-wide guarantee must fail
+                // closed rather than warn like the deprecated raw options.
+                $this->options[\CURLMOPT_PIPELINING] = 0;
+                $this->requiredOptions[\CURLMOPT_PIPELINING] = true;
+            }
+        }
 
 
         // unsetting the property forces the first access to go through
         // unsetting the property forces the first access to go through
         // __get().
         // __get().
@@ -91,8 +283,9 @@ class CurlMultiHandler
      *
      *
      * @return resource|\CurlMultiHandle
      * @return resource|\CurlMultiHandle
      *
      *
-     * @throws \BadMethodCallException when another field as `_mh` will be gotten
-     * @throws \RuntimeException       when curl can not initialize a multi handle
+     * @throws \BadMethodCallException   when another field as `_mh` will be gotten
+     * @throws \RuntimeException         when curl can not initialize a multi handle
+     * @throws \InvalidArgumentException when a required cURL multi option cannot be applied
      */
      */
     public function __get($name)
     public function __get($name)
     {
     {
@@ -106,76 +299,704 @@ class CurlMultiHandler
             throw new \RuntimeException('Can not initialize curl multi handle.');
             throw new \RuntimeException('Can not initialize curl multi handle.');
         }
         }
 
 
-        $this->_mh = $multiHandle;
+        try {
+            foreach ($this->options as $option => $value) {
+                if (true === @curl_multi_setopt($multiHandle, $option, $value)) {
+                    continue;
+                }
 
 
-        foreach ($this->options as $option => $value) {
-            // A warning is raised in case of a wrong option.
-            curl_multi_setopt($this->_mh, $option, $value);
+                if (isset($this->requiredOptions[$option])) {
+                    // A first-class option such as a connection cap must
+                    // never be silently dropped.
+                    throw new \InvalidArgumentException(\sprintf('Unable to apply the cURL multi option %s; it was rejected by the runtime libcurl.', self::formatCurlMultiOption($option)));
+                }
+
+                \trigger_error(\sprintf('Unable to apply the cURL multi option %s; it was ignored by the runtime libcurl.', self::formatCurlMultiOption($option)), \E_USER_WARNING);
+            }
+        } catch (\Throwable $e) {
+            // Do not publish a partially configured handle; a later access
+            // retries the initialization from scratch.
+            try {
+                \curl_multi_close($multiHandle);
+            } catch (\Throwable $ignored) {
+                // Preserve the original failure.
+            }
+
+            throw $e;
         }
         }
 
 
+        $this->_mh = $multiHandle;
+
         return $this->_mh;
         return $this->_mh;
     }
     }
 
 
     public function __destruct()
     public function __destruct()
     {
     {
         if (isset($this->_mh)) {
         if (isset($this->_mh)) {
-            \curl_multi_close($this->_mh);
-            unset($this->_mh);
+            try {
+                \curl_multi_close($this->_mh);
+            } catch (\Throwable $e) {
+                // Destructors must not throw.
+            } finally {
+                unset($this->_mh);
+            }
         }
         }
     }
     }
 
 
     public function __invoke(RequestInterface $request, array $options): PromiseInterface
     public function __invoke(RequestInterface $request, array $options): PromiseInterface
     {
     {
+        HostValidator::assertRequestHost($request);
+
+        if ($this->connectionCapsApplied
+            && \defined('CURLOPT_SHARE')
+            && isset($options['curl'])
+            && \is_array($options['curl'])
+            && \array_key_exists((int) \constant('CURLOPT_SHARE'), $options['curl'])
+        ) {
+            // Key presence alone conflicts: Guzzle cannot verify that a
+            // caller-managed shared connection pool honors the caps.
+            throw new \InvalidArgumentException('The request-level CURLOPT_SHARE cURL option cannot be combined with CurlMultiHandler connection cap options because Guzzle cannot verify that an external shared connection pool honors cURL multi connection caps.');
+        }
+
         $easy = $this->factory->create($request, $options);
         $easy = $this->factory->create($request, $options);
+
+        try {
+            $this->rejectMultiplexPipeliningConflict($easy, $options);
+            $this->applyMultiplexNone($easy, $options);
+            $this->applyProxyTunnelOwnership($easy);
+        } catch (\Throwable $e) {
+            try {
+                $this->factory->release($easy);
+            } catch (\Throwable $releaseFailure) {
+                // Preserve the original failure.
+            }
+
+            throw $e;
+        }
+
         $id = (int) $easy->handle;
         $id = (int) $easy->handle;
 
 
+        $waitToken = new \stdClass();
+
+        $promise = null;
         $promise = new Promise(
         $promise = new Promise(
-            [$this, 'execute'],
-            function () use ($id) {
-                return $this->cancel($id);
+            function () use ($id, $waitToken, $easy, &$promise): void {
+                // Waiting cannot drive native cURL while a callback has the
+                // multi handle busy; fail the wait promptly instead of
+                // self-deadlocking.
+                $idReused = $this->multiExecDepth > 0
+                    ? $this->failNestedWait($id, $waitToken)
+                    : $this->executeUntil($id, $waitToken);
+
+                // Settling can be queued, and guzzlehttp/promises drains the
+                // queue before deciding a wait function achieved nothing.
+                P\Utils::queue()->run();
+
+                // Never null: assigned below before any wait can invoke this.
+                /** @var Promise $promise */
+                if (!P\Is::pending($promise)) {
+                    return;
+                }
+
+                // Neither path guarantees the transfer settled, and returning
+                // while pending makes guzzlehttp/promises reject with a bare
+                // string naming nothing.
+                $stalled = $idReused
+                    ? 'its native cURL handle ID was reused by another request'
+                    : 'its entry was removed without settling';
+
+                $message = \sprintf('Waiting on cURL multi handler transfer %d cannot make progress (%s).', $id, $stalled);
+
+                // The entry is gone or belongs to another request, so
+                // attribute from this easy handle.
+                $promise->reject(new RequestException($message, $easy->request, $easy->response));
+            },
+            function () use ($id, $waitToken) {
+                return $this->cancel($id, $waitToken);
             }
             }
         );
         );
 
 
-        $this->addRequest(['easy' => $easy, 'deferred' => $promise]);
+        $entry = ['easy' => $easy, 'deferred' => $promise, 'wait_token' => $waitToken];
+
+        try {
+            $this->addRequest($entry);
+        } catch (\Throwable $e) {
+            throw $this->discardPendingRequest($id, $entry, $e);
+        }
 
 
         return $promise;
         return $promise;
     }
     }
 
 
+    /**
+     * The "multiplex" request option sets CURLOPT_PIPEWAIT, which libcurl
+     * ignores entirely when the multi handle's CURLMOPT_PIPELINING option
+     * disables multiplexing, so an explicit request for multiplexing on a
+     * handler configured against it is a configuration error. The required
+     * family conflicts marker-independently: a required guarantee on a handler
+     * that disables multiplexing is contradictory even when the transfer would
+     * not wait. A raw CURLOPT_PIPEWAIT cURL option conflicts with every
+     * explicit mode on this handler, where waiting is operationally
+     * meaningful: whatever its value, it is a second wait/eager authority
+     * applied after the mode's own decision.
+     */
+    private function rejectMultiplexPipeliningConflict(EasyHandle $easy, array $options): void
+    {
+        $multiplex = $options['multiplex'] ?? null;
+
+        if (null === $multiplex) {
+            return;
+        }
+
+        if (\defined('CURLOPT_PIPEWAIT')
+            && isset($options['curl'])
+            && \is_array($options['curl'])
+            && \array_key_exists((int) \constant('CURLOPT_PIPEWAIT'), $options['curl'])
+        ) {
+            // Key presence alone conflicts, and it must be rejected before
+            // the marker below is consulted: the marker reflects the final
+            // merged configuration, which the raw value has falsified.
+            throw new \InvalidArgumentException('The "multiplex" request option cannot be combined with the raw CURLOPT_PIPEWAIT cURL option on the cURL multi handler; remove the raw option.');
+        }
+
+        if (Multiplexing::WAIT === $multiplex && !$easy->usesPipewait) {
+            // Explicit wait only conflicts when the transfer would actually
+            // wait; an HTTP/1.1 wait request never sets the marker.
+            return;
+        }
+
+        if (!\in_array($multiplex, [Multiplexing::WAIT, Multiplexing::REQUIRE_EAGER, Multiplexing::REQUIRE_WAIT], true)) {
+            return;
+        }
+
+        if ($this->multiplexDisabled) {
+            // Checked before the raw option: the handler wrote its own
+            // CURLMOPT_PIPELINING value when "multiplex" disabled it.
+            throw new \InvalidArgumentException('The "multiplex" request option cannot be combined with a CurlMultiHandler whose "multiplex" option is Multiplexing::NONE; remove the handler option or set the request option to "eager".');
+        }
+
+        if (!\is_array($this->options) || !\array_key_exists(\CURLMOPT_PIPELINING, $this->options)) {
+            // A legacy non-array "options" value is tolerated by the
+            // constructor and cannot contain the option.
+            return;
+        }
+
+        $pipelining = $this->options[\CURLMOPT_PIPELINING];
+        if (!\is_scalar($pipelining)) {
+            // ext-curl derives the integer mask from non-scalar values with
+            // type-dependent zval semantics, so the effective mask cannot be
+            // predicted here; require an explicit integer instead.
+            throw new \InvalidArgumentException('The CurlMultiHandler CURLMOPT_PIPELINING option must be an integer when combined with the "multiplex" request option.');
+        }
+
+        $multiplexBit = \defined('CURLPIPE_MULTIPLEX') ? \CURLPIPE_MULTIPLEX : 2;
+        if (((int) $pipelining & $multiplexBit) !== 0) {
+            return;
+        }
+
+        throw new \InvalidArgumentException('The "multiplex" request option cannot be combined with a CurlMultiHandler CURLMOPT_PIPELINING option that disables multiplexing; set CURLMOPT_PIPELINING to CURLPIPE_MULTIPLEX, remove the option, or set the "multiplex" option to "eager".');
+    }
+
+    /**
+     * A Multiplexing::NONE request option is a sole-use guarantee: the
+     * transfer must not share its connection with any concurrent transfer.
+     * It holds structurally on a handler whose "multiplex" option is
+     * Multiplexing::NONE, and for HTTP/1.x transfers, which never join a
+     * multiplexed connection and open connections nothing can join. An
+     * HTTP/2 request on a handler that multiplexes is rejected, as is any
+     * configuration under which the guarantee cannot be verified (custom
+     * handle factories control the native handle) or cannot be hardened
+     * (challenge-response authentication retries and Expect 417 retries
+     * re-enter connection selection as internal follows, which disarm
+     * CURLOPT_FRESH_CONNECT). A raw CURLMOPT_PIPELINING multi option, and
+     * deprecated-but-applied raw cURL options that can defeat the declared
+     * protocol version, retry through internal follows, or replace the
+     * managed header list, are rejected by key presence. On runtimes whose
+     * matcher can hand an HTTP/1.x transfer an idle multiplexed connection
+     * (below libcurl 7.77.0, and 8.11.0-8.12.1), accepted transfers force
+     * a fresh connection.
+     */
+    private function applyMultiplexNone(EasyHandle $easy, array $options): void
+    {
+        if (Multiplexing::NONE !== ($options['multiplex'] ?? null) || $this->multiplexDisabled) {
+            return;
+        }
+
+        if (\defined('CURLMOPT_PIPELINING') && \is_array($this->options) && \array_key_exists(\CURLMOPT_PIPELINING, $this->options)) {
+            // Key presence alone conflicts, matching the constructor's rule
+            // for the named option: raw multi options that fail to apply only
+            // warn (they are not in requiredOptions), so even an agreeing
+            // zero mask cannot prove the guarantee. is_array: legacy non-array
+            // "options" values are deprecated but still stored.
+            throw new \InvalidArgumentException('The "multiplex" request option cannot be Multiplexing::NONE alongside a raw CURLMOPT_PIPELINING cURL multi option; replace the raw option with the "multiplex" cURL multi handler option.');
+        }
+
+        if ($this->customHandleFactory) {
+            throw new \InvalidArgumentException('The "multiplex" request option can only be Multiplexing::NONE on a CurlMultiHandler with a custom "handle_factory" when the handler\'s own "multiplex" option is Multiplexing::NONE, because the guarantee is enforced against the native easy handle the factory controls.');
+        }
+
+        $version = $easy->request->getProtocolVersion();
+        if ('2' === $version || '2.0' === $version) {
+            throw new \InvalidArgumentException('The "multiplex" request option can only be Multiplexing::NONE for an HTTP/1.x request on a CurlMultiHandler that permits multiplexing; set the "multiplex" client or CurlMultiHandler constructor option to Multiplexing::NONE to disable multiplexing for every transfer, or send the request with its "version" option set to "1.1".');
+        }
+
+        if (isset($options['curl']) && \is_array($options['curl'])) {
+            foreach (['CURLOPT_HTTP_VERSION', 'CURLOPT_HTTPAUTH', 'CURLOPT_PROXYAUTH', 'CURLOPT_FOLLOWLOCATION', 'CURLOPT_HTTPHEADER', 'CURLOPT_ALTSVC', 'CURLOPT_ALTSVC_CTRL', 'CURLOPT_PROXYTYPE'] as $constant) {
+                if (\defined($constant) && \array_key_exists((int) \constant($constant), $options['curl'])) {
+                    // Key presence alone conflicts. A raw CURLOPT_HTTP_VERSION
+                    // overrides the declared version after the factory
+                    // mapping, and raw alt-svc options or an HTTPS2 proxy
+                    // type can put a declared-HTTP/1.x transfer on a joinable
+                    // HTTP/2 connection; raw challenge-response
+                    // authentication (origin 401 or proxy 407) and native
+                    // redirects re-enter connection selection as internal
+                    // follows, which disarm CURLOPT_FRESH_CONNECT, so the
+                    // hardening below cannot cover them; a raw
+                    // CURLOPT_HTTPHEADER replaces the managed header list,
+                    // including the Expect suppression the check below
+                    // relies on.
+                    throw new \InvalidArgumentException(\sprintf('The "multiplex" request option cannot be Multiplexing::NONE combined with the raw %s cURL option on a CurlMultiHandler that permits multiplexing; remove the raw option, or set the "multiplex" client or CurlMultiHandler constructor option to Multiplexing::NONE.', $constant));
+                }
+            }
+        }
+
+        if (Psr7\Utils::caselessContains($easy->request->getHeaderLine('Expect'), '100-continue')) {
+            // libcurl arms its Expect handling by a caseless substring scan
+            // of the header value (Curl_compareheader), so any value
+            // containing 100-continue can make a 417 response retry as an
+            // internal follow, which disarms CURLOPT_FRESH_CONNECT; requests
+            // without the header are safe because the factory suppresses
+            // libcurl's automatic Expect.
+            throw new \InvalidArgumentException('The "multiplex" request option cannot be Multiplexing::NONE for a request carrying an "Expect: 100-continue" header on a CurlMultiHandler that permits multiplexing; remove the explicitly supplied "Expect" header, set the "expect" request option to false to prevent it being added automatically, or set the "multiplex" client or CurlMultiHandler constructor option to Multiplexing::NONE.');
+        }
+
+        if (CurlVersion::supportsHttpVersionReuseMatching()) {
+            return;
+        }
+
+        // Unqualified curl_setopt so the test bootstrap shadow records it.
+        if (true !== curl_setopt($easy->handle, \CURLOPT_FRESH_CONNECT, true)) {
+            // The hardening is the guarantee on these runtimes; failing to
+            // apply it must fail closed, mirroring applyCurlOptions().
+            throw new \InvalidArgumentException('Unable to set cURL option CURLOPT_FRESH_CONNECT.');
+        }
+    }
+
+    /**
+     * @param array<mixed> $options
+     */
+    private static function triggerConflictingCurlMultiOptionDeprecations(array $options): void
+    {
+        if ($options === []) {
+            return;
+        }
+
+        $conflictingOptions = self::conflictingCurlMultiOptions();
+        $sinceOverrides = self::conflictingCurlMultiOptionSinceOverrides();
+        foreach ($options as $option => $_) {
+            if (\array_key_exists($option, $conflictingOptions)) {
+                \trigger_deprecation('guzzlehttp/guzzle', $sinceOverrides[$option] ?? '7.14', \sprintf('Passing %s in the cURL multi handler "options" is deprecated; guzzlehttp/guzzle 8.0 will reject this option. Use %s instead.', self::formatCurlMultiOption($option), $conflictingOptions[$option]));
+            }
+        }
+    }
+
+    /**
+     * @return array<int, string>
+     */
+    private static function conflictingCurlMultiOptionSinceOverrides(): array
+    {
+        if (!\defined('CURLMOPT_PIPELINING')) {
+            // Matches conflictingCurlMultiOptions(): ext-curl builds against
+            // pre-7.16 libcurl headers do not define the constant.
+            return [];
+        }
+
+        return [\CURLMOPT_PIPELINING => '7.15'];
+    }
+
+    /**
+     * @param array<mixed> $options
+     */
+    private static function hasConnectionCapOption(array $options): bool
+    {
+        foreach (self::CONNECTION_CAP_OPTIONS as $name => $_) {
+            if (($options[$name] ?? null) !== null) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * @param array<mixed> $constructorOptions
+     * @param array<mixed> $multiOptions
+     */
+    private static function rejectConnectionCapOptionConflicts(array $constructorOptions, array $multiOptions): void
+    {
+        foreach (self::CONNECTION_CAP_OPTIONS as $name => $constant) {
+            if (($constructorOptions[$name] ?? null) === null || !\defined($constant)) {
+                continue;
+            }
+
+            $option = \constant($constant);
+            if (\array_key_exists($option, $multiOptions)) {
+                throw new \InvalidArgumentException(\sprintf('%s conflicts with a %s entry in the "options" array.', $name, $constant));
+            }
+        }
+    }
+
+    /**
+     * @param array<mixed> $options
+     */
+    private function addConnectionCapOptions(array $options): void
+    {
+        foreach (self::CONNECTION_CAP_OPTIONS as $name => $constant) {
+            $value = $options[$name] ?? null;
+            if ($value === null) {
+                continue;
+            }
+
+            if (!\is_int($value) || $value < 1) {
+                throw new \InvalidArgumentException(\sprintf('%s must be a positive integer.', $name));
+            }
+
+            CurlVersion::ensureConnectionCapsSupported($name);
+
+            $option = \constant($constant);
+            if (\array_key_exists($option, $this->options)) {
+                throw new \InvalidArgumentException(\sprintf('%s conflicts with a %s entry in the "options" array.', $name, $constant));
+            }
+
+            $this->options[$option] = $value;
+            $this->requiredOptions[$option] = true;
+            $this->connectionCapsApplied = true;
+        }
+    }
+
+    /**
+     * @param int|string $option
+     */
+    private static function formatCurlMultiOption($option): string
+    {
+        if (!\is_int($option)) {
+            return \sprintf('"%s"', $option);
+        }
+
+        static $names = null;
+
+        if (null === $names) {
+            $names = [];
+            foreach (\get_defined_constants(true)['curl'] ?? [] as $name => $value) {
+                if (\is_int($value) && \strpos($name, 'CURLMOPT_') === 0 && !isset($names[$value])) {
+                    $names[$value] = $name;
+                }
+            }
+        }
+
+        if (isset($names[$option])) {
+            return \sprintf('%s (%d)', $names[$option], $option);
+        }
+
+        return (string) $option;
+    }
+
+    /**
+     * @return array<int, string>
+     */
+    private static function conflictingCurlMultiOptions(): array
+    {
+        static $options = null;
+
+        if ($options !== null) {
+            return $options;
+        }
+
+        $options = [];
+
+        self::addConflictingCurlMultiOption($options, 'CURLMOPT_MAX_HOST_CONNECTIONS', 'the "max_host_connections" client option or cURL multi handler option');
+        self::addConflictingCurlMultiOption($options, 'CURLMOPT_MAX_TOTAL_CONNECTIONS', 'the "max_total_connections" client option or cURL multi handler option');
+        self::addConflictingCurlMultiOption($options, 'CURLMOPT_PIPELINING', 'Multiplexing::NONE via the "multiplex" cURL multi handler or client option to disable multiplexing, or remove the raw option for the runtime default (multiplexing defaults on from libcurl 7.62, except 7.65.0 and 7.65.1)');
+
+        return $options;
+    }
+
+    /**
+     * @param array<int, string> $options
+     */
+    private static function addConflictingCurlMultiOption(array &$options, string $constant, string $replacement): void
+    {
+        if (!\defined($constant)) {
+            return;
+        }
+
+        $value = \constant($constant);
+        if (\is_int($value)) {
+            $options[$value] = $replacement;
+        }
+    }
+
+    /**
+     * Isolates the connection cache when the request's proxy tunnel section
+     * differs from the one the multi handle's cache may already hold.
+     */
+    private function applyProxyTunnelOwnership(EasyHandle $easy): void
+    {
+        $signature = $easy->proxyTunnelSignature;
+        if ($signature === null || $signature === $this->proxyTunnelOwner) {
+            return;
+        }
+
+        if ($this->proxyTunnelOwner === null) {
+            // No in-domain transfer has ever run on this multi handle: latch
+            // the owner without destroying pooled direct connections.
+            $this->proxyTunnelOwner = $signature;
+
+            return;
+        }
+
+        if (
+            $this->handles === []
+            && 0 === $this->multiExecDepth
+            && 0 === $this->messageProcessingDepth
+            && $this->deferredCancels === []
+        ) {
+            // Idle: hand the connection cache over by recreating the multi
+            // handle (unsetting re-arms the lazy __get initializer, which
+            // re-applies the CURLMOPT_* options).
+            if (isset($this->_mh)) {
+                \curl_multi_close($this->_mh);
+                unset($this->_mh);
+            }
+            $this->proxyTunnelOwner = $signature;
+
+            return;
+        }
+
+        // Busy: isolate this transfer from the owner's pooled tunnels.
+        $this->isolateProxyTunnelTransfer($easy);
+    }
+
+    private function addCurlHandle(EasyHandle $easy): void
+    {
+        $this->isolateFromForeignActiveProxyTunnel($easy);
+
+        // Unqualified curl_multi_add_handle so the test bootstrap shadow can
+        // override the result.
+        $result = curl_multi_add_handle($this->_mh, $easy->handle);
+
+        if (\CURLM_OK !== $result) {
+            if (\PHP_VERSION_ID < 80226 || (\PHP_VERSION_ID >= 80300 && \PHP_VERSION_ID < 80314)) {
+                // Before PHP 8.2.26 and 8.3.14, ext-curl kept the easy handle
+                // in its multi bookkeeping even when the native add failed
+                // (https://github.com/php/php-src/pull/16302); remove it so
+                // the handle can be pooled or closed safely.
+                \curl_multi_remove_handle($this->_mh, $easy->handle);
+            }
+
+            throw new RequestException(\sprintf('Unable to add the cURL handle to the cURL multi handler: %s (%d).', (string) \curl_multi_strerror($result), $result), $easy->request);
+        }
+
+        $this->markProxyTunnelActive($easy);
+
+        $id = (int) $easy->handle;
+        if (isset($this->handles[$id])) {
+            $this->handles[$id]['attached'] = true;
+        }
+    }
+
+    /**
+     * @param resource|\CurlHandle $handle
+     */
+    private function removeCompletedHandleFromMulti(int $id, $handle): void
+    {
+        $this->removeHandleFromMulti($handle);
+        $this->unmarkProxyTunnelActiveById($id);
+    }
+
+    /**
+     * Removes a transfer from the multi handle under the native execution
+     * guard: removing a still-running transfer performs a final progress
+     * update that can run a user progress callback.
+     *
+     * @param resource|\CurlHandle $handle
+     */
+    private function removeHandleFromMulti($handle): void
+    {
+        ++$this->multiExecDepth;
+
+        try {
+            \curl_multi_remove_handle($this->_mh, $handle);
+        } finally {
+            --$this->multiExecDepth;
+            $this->finishDeferredWork();
+        }
+    }
+
+    private function isolateFromForeignActiveProxyTunnel(EasyHandle $easy): void
+    {
+        $signature = $easy->proxyTunnelSignature;
+
+        if ($signature === null || $this->activeProxyTunnelSignatures === []) {
+            return;
+        }
+
+        if (\count($this->activeProxyTunnelSignatures) === 1 && isset($this->activeProxyTunnelSignatures[$signature])) {
+            return;
+        }
+
+        $this->isolateProxyTunnelTransfer($easy);
+    }
+
+    private function isolateProxyTunnelTransfer(EasyHandle $easy): void
+    {
+        foreach (self::PROXY_TUNNEL_ISOLATION_OPTIONS as $name) {
+            try {
+                // Unqualified curl_setopt so the test bootstrap shadow records it.
+                $applied = curl_setopt($easy->handle, (int) \constant($name), true);
+            } catch (\Throwable $e) {
+                throw new RequestException(self::proxyTunnelIsolationFailureMessage($name), $easy->request, null, $e);
+            }
+
+            if (true !== $applied) {
+                throw new RequestException(self::proxyTunnelIsolationFailureMessage($name), $easy->request);
+            }
+        }
+    }
+
+    private static function proxyTunnelIsolationFailureMessage(string $name): string
+    {
+        return \sprintf('Unable to apply the %s cURL option required to isolate the transfer from foreign proxy tunnel connections.', $name);
+    }
+
+    private function markProxyTunnelActive(EasyHandle $easy): void
+    {
+        $signature = $easy->proxyTunnelSignature;
+        if ($signature === null) {
+            return;
+        }
+
+        $id = (int) $easy->handle;
+        if (isset($this->activeProxyTunnelHandles[$id])) {
+            if ($this->activeProxyTunnelHandles[$id] === $signature) {
+                return;
+            }
+
+            $this->unmarkProxyTunnelActiveById($id);
+        }
+
+        $this->activeProxyTunnelHandles[$id] = $signature;
+        $this->activeProxyTunnelSignatures[$signature] = ($this->activeProxyTunnelSignatures[$signature] ?? 0) + 1;
+    }
+
+    private function unmarkProxyTunnelActive(EasyHandle $easy): void
+    {
+        $this->unmarkProxyTunnelActiveById((int) $easy->handle);
+    }
+
+    private function unmarkProxyTunnelActiveById(int $id): void
+    {
+        if (!isset($this->activeProxyTunnelHandles[$id])) {
+            return;
+        }
+
+        $signature = $this->activeProxyTunnelHandles[$id];
+        unset($this->activeProxyTunnelHandles[$id]);
+
+        if (!isset($this->activeProxyTunnelSignatures[$signature])) {
+            return;
+        }
+
+        --$this->activeProxyTunnelSignatures[$signature];
+
+        if ($this->activeProxyTunnelSignatures[$signature] <= 0) {
+            unset($this->activeProxyTunnelSignatures[$signature]);
+        }
+    }
+
     /**
     /**
      * Ticks the curl event loop.
      * Ticks the curl event loop.
      */
      */
     public function tick(): void
     public function tick(): void
     {
     {
-        // Add any delayed handles if needed.
-        if ($this->delays) {
+        $this->tickFor(null, null);
+    }
+
+    /**
+     * Ticks the curl event loop, returning before the blocking select if the
+     * targeted transfer has settled, been canceled, or been replaced by a
+     * request that reused its native handle ID.
+     */
+    private function tickFor(?int $targetId, ?object $waitToken): void
+    {
+        // Add any delayed handles if needed. Attachment is skipped while a
+        // callback has native execution busy; the outer frame attaches due
+        // transfers once it unwinds.
+        if ($this->delays && 0 === $this->multiExecDepth) {
             $currentTime = Utils::currentTime();
             $currentTime = Utils::currentTime();
             foreach ($this->delays as $id => $delay) {
             foreach ($this->delays as $id => $delay) {
                 if ($currentTime >= $delay) {
                 if ($currentTime >= $delay) {
+                    $entry = $this->handles[$id];
                     unset($this->delays[$id]);
                     unset($this->delays[$id]);
-                    \curl_multi_add_handle(
-                        $this->_mh,
-                        $this->handles[$id]['easy']->handle
-                    );
+
+                    try {
+                        $this->addCurlHandle($entry['easy']);
+                    } catch (\Throwable $e) {
+                        // The promise has already escaped, so reject it
+                        // rather than throw.
+                        $rejection = $this->discardPendingRequest($id, $entry, $e);
+                        if (P\Is::pending($entry['deferred'])) {
+                            $entry['deferred']->reject($rejection);
+                        }
+                    }
                 }
                 }
             }
             }
         }
         }
 
 
-        // Run curl_multi_exec in the queue to enable other async tasks to run
-        P\Utils::queue()->add(Closure::fromCallable([$this, 'tickInQueue']));
+        // Run curl_multi_exec in the queue to enable other async tasks to
+        // run, surface completions, and drain any work they queued so a
+        // ready cancellation or new transfer is not held behind the select.
+        do {
+            P\Utils::queue()->add(Closure::fromCallable([$this, 'tickInQueue']));
+
+            // Step through the task queue which may add additional requests.
+            P\Utils::queue()->run();
 
 
-        // Step through the task queue which may add additional requests.
-        P\Utils::queue()->run();
+            if ($this->multiExecDepth > 0) {
+                // A cURL callback re-entered the handler while native
+                // execution is running; the outer frame drives native cURL
+                // once it unwinds.
+                return;
+            }
+
+            if (isset($this->_mh)) {
+                $this->processMessages();
+            }
+        } while (!P\Utils::queue()->isEmpty());
 
 
-        if ($this->active && \curl_multi_select($this->_mh, $this->selectTimeout) === -1) {
+        if (!isset($this->_mh)) {
+            // Nothing is attached natively (or initialization just failed);
+            // there is nothing to run and nothing to recreate the handle for.
+            return;
+        }
+
+        if ($targetId !== null && !$this->hasRequest($targetId, $waitToken)) {
+            return;
+        }
+
+        if ($this->active && \curl_multi_select($this->_mh, $this->effectiveSelectTimeout()) === -1) {
             // Perform a usleep if a select returns -1.
             // Perform a usleep if a select returns -1.
             // See: https://bugs.php.net/bug.php?id=61141
             // See: https://bugs.php.net/bug.php?id=61141
             \usleep(250);
             \usleep(250);
         }
         }
 
 
-        while (\curl_multi_exec($this->_mh, $this->active) === \CURLM_CALL_MULTI_PERFORM) {
+        do {
+            $exec = $this->executeMulti();
+
             // Prevent busy looping for slow HTTP requests.
             // Prevent busy looping for slow HTTP requests.
-            \curl_multi_select($this->_mh, $this->selectTimeout);
-        }
+            if ($exec === \CURLM_CALL_MULTI_PERFORM) {
+                \curl_multi_select($this->_mh, $this->effectiveSelectTimeout());
+            }
+        } while ($exec === \CURLM_CALL_MULTI_PERFORM);
 
 
         $this->processMessages();
         $this->processMessages();
     }
     }
@@ -185,100 +1006,447 @@ class CurlMultiHandler
      */
      */
     private function tickInQueue(): void
     private function tickInQueue(): void
     {
     {
-        if (\curl_multi_exec($this->_mh, $this->active) === \CURLM_CALL_MULTI_PERFORM) {
+        if ($this->multiExecDepth > 0) {
+            // A cURL callback re-entered the handler while native execution
+            // is running; the outer frame drives native cURL once it unwinds.
+            return;
+        }
+
+        if (!isset($this->_mh)) {
+            // Nothing is attached natively (or initialization just failed);
+            // there is nothing to run and nothing to recreate the handle for.
+            return;
+        }
+
+        $exec = $this->executeMulti();
+
+        if ($exec === \CURLM_CALL_MULTI_PERFORM) {
             \curl_multi_select($this->_mh, 0);
             \curl_multi_select($this->_mh, 0);
             P\Utils::queue()->add(Closure::fromCallable([$this, 'tickInQueue']));
             P\Utils::queue()->add(Closure::fromCallable([$this, 'tickInQueue']));
         }
         }
     }
     }
 
 
+    /**
+     * @phpstan-impure
+     */
+    private function executeMulti(): int
+    {
+        ++$this->multiExecDepth;
+
+        try {
+            return \curl_multi_exec($this->_mh, $this->active);
+        } finally {
+            --$this->multiExecDepth;
+            $this->finishDeferredWork();
+        }
+    }
+
+    /**
+     * Flushes cancels and attachments deferred while the multi handle was
+     * busy executing transfers or removing a handle.
+     */
+    private function finishDeferredWork(): void
+    {
+        if ($this->multiExecDepth > 0 || $this->finishingDeferredWork) {
+            // A nested frame (a cURL callback re-entered the handler) must
+            // not flush while an outer frame is still using the multi
+            // handle; the outermost frame flushes once it unwinds.
+            return;
+        }
+
+        $this->finishingDeferredWork = true;
+
+        try {
+            $failure = null;
+
+            // Removing a cancelled transfer runs its final progress update,
+            // whose callback can cancel other transfers or create requests;
+            // drain until no deferred work remains.
+            do {
+                $this->cleanupDeferredCancels($failure);
+                $this->flushDeferredAdds();
+            } while ($this->deferredCancels !== [] || $this->deferredAdds !== []);
+
+            if ($failure !== null) {
+                throw $failure;
+            }
+        } finally {
+            $this->finishingDeferredWork = false;
+        }
+    }
+
     /**
     /**
      * Runs until all outstanding connections have completed.
      * Runs until all outstanding connections have completed.
      */
      */
     public function execute(): void
     public function execute(): void
     {
     {
+        if ($this->multiExecDepth > 0) {
+            // Native cURL cannot be driven while a callback has it busy, so
+            // the loop would spin without ever progressing.
+            throw new \LogicException('Cannot run the cURL multi event loop from inside a cURL callback; the callback must return before transfers can progress.');
+        }
+
         $queue = P\Utils::queue();
         $queue = P\Utils::queue();
 
 
         while ($this->handles || !$queue->isEmpty()) {
         while ($this->handles || !$queue->isEmpty()) {
-            // If there are no transfers, then sleep for the next delay
-            if (!$this->active && $this->delays) {
+            // If there are no transfers, then sleep for the next delay,
+            // unless ready queue work could change what is pending.
+            if (!$this->active && $this->delays && $queue->isEmpty()) {
                 \usleep($this->timeToNext());
                 \usleep($this->timeToNext());
             }
             }
             $this->tick();
             $this->tick();
         }
         }
     }
     }
 
 
+    /**
+     * Runs the event loop until the given transfer has finished, so waiting
+     * on a promise does not wait for every other transfer on the handler
+     * like execute() does.
+     *
+     * The native cURL handle ID can be reused by a request created from a
+     * completion callback, so the wait token guards against waiting on an
+     * unrelated transfer that inherited the ID.
+     *
+     * @return bool Whether another request had reused the native cURL handle
+     *              ID by the time the loop stopped
+     */
+    private function executeUntil(int $id, object $waitToken): bool
+    {
+        $queue = P\Utils::queue();
+
+        while ($this->hasRequest($id, $waitToken)) {
+            // If the transfer is delayed, then sleep until it is due, unless
+            // ready queue work could cancel or replace it first.
+            if (!$this->active && isset($this->delays[$id]) && $queue->isEmpty()) {
+                \usleep($this->timeToNext());
+            }
+            $this->tickFor($id, $waitToken);
+        }
+
+        // Sample before the drain below, which can add or remove an entry
+        // under this ID and so rewrite the answer.
+        $idReused = isset($this->handles[$id]);
+
+        if (!$queue->isEmpty()) {
+            $queue->run();
+        }
+
+        return $idReused;
+    }
+
+    /**
+     * Checks that the request with the given handle ID is still pending and,
+     * when a wait token is given, has not been replaced by a request that
+     * reused the ID.
+     */
+    private function hasRequest(int $id, ?object $waitToken = null): bool
+    {
+        if (!isset($this->handles[$id])) {
+            return false;
+        }
+
+        return $waitToken === null || ($this->handles[$id]['wait_token'] ?? null) === $waitToken;
+    }
+
     private function addRequest(array $entry): void
     private function addRequest(array $entry): void
     {
     {
         $easy = $entry['easy'];
         $easy = $entry['easy'];
         $id = (int) $easy->handle;
         $id = (int) $easy->handle;
+        $entry['attached'] = false;
+
+        $displaced = $this->handles[$id] ?? null;
+        if ($displaced !== null) {
+            // Never silently discard a tracked entry; settle it first.
+            unset($this->handles[$id], $this->delays[$id], $this->deferredAdds[$id]);
+            if (P\Is::pending($displaced['deferred'])) {
+                $message = \sprintf('cURL multi handler transfer %d was displaced by another request that reused its native cURL handle ID.', $id);
+                $displaced['deferred']->reject(new RequestException($message, $displaced['easy']->request, $displaced['easy']->response));
+            }
+        }
+
         $this->handles[$id] = $entry;
         $this->handles[$id] = $entry;
-        if (empty($easy->options['delay'])) {
-            \curl_multi_add_handle($this->_mh, $easy->handle);
-        } else {
+
+        if (!empty($easy->options['delay'])) {
             $this->delays[$id] = Utils::currentTime() + ($easy->options['delay'] / 1000);
             $this->delays[$id] = Utils::currentTime() + ($easy->options['delay'] / 1000);
+        } elseif ($this->multiExecDepth > 0) {
+            // A request created from inside a cURL callback cannot be added
+            // natively while curl_multi_exec() is running; libcurl 7.59+
+            // rejects the recursive call. Attach it once the outermost
+            // native execution unwinds.
+            $this->deferredAdds[$id] = $entry['wait_token'] ?? null;
+        } else {
+            $this->addCurlHandle($easy);
+        }
+    }
+
+    /**
+     * Rolls back a request that can no longer be attached, releasing the
+     * easy handle exactly once and preserving the original failure.
+     *
+     * @param array{easy: EasyHandle, deferred: Promise, wait_token?: object|null, attached?: bool} $entry
+     */
+    private function discardPendingRequest(int $id, array $entry, \Throwable $failure): \Throwable
+    {
+        unset($this->handles[$id], $this->delays[$id], $this->deferredAdds[$id]);
+
+        try {
+            $this->factory->release($entry['easy']);
+        } catch (\Throwable $e) {
+            // Preserve the original failure.
+        }
+
+        return $failure;
+    }
+
+    /**
+     * Fails a synchronous wait attempted from inside a cURL callback, where
+     * native execution cannot progress until the callback returns.
+     *
+     * @return bool Whether another request had reused the native cURL handle
+     *              ID, which only matters when no transfer was left to fail
+     */
+    private function failNestedWait(int $id, object $token): bool
+    {
+        if (!$this->hasRequest($id, $token)) {
+            // Nothing left to fail, so report which way the entry went.
+            return isset($this->handles[$id]);
+        }
+
+        $entry = $this->handles[$id];
+        $failure = new RequestException('Cannot synchronously wait for a transfer from inside a cURL callback on the same cURL multi handler; the callback must return before the transfer can progress.', $entry['easy']->request, $entry['easy']->response);
+
+        if (!empty($entry['attached'])) {
+            // Native removal must wait until the outermost execution unwinds.
+            unset($this->handles[$id], $this->delays[$id], $this->deferredAdds[$id]);
+            $this->deferredCancels[$id] = ['easy' => $entry['easy'], 'attached' => true];
+        } else {
+            $this->discardPendingRequest($id, $entry, $failure);
+        }
+
+        $entry['deferred']->reject($failure);
+
+        return false;
+    }
+
+    /**
+     * Attaches requests whose native attachment was deferred because they
+     * were created from inside a cURL callback.
+     */
+    private function flushDeferredAdds(): void
+    {
+        if ($this->deferredAdds === []) {
+            return;
+        }
+
+        $adds = $this->deferredAdds;
+        $this->deferredAdds = [];
+
+        foreach ($adds as $id => $token) {
+            if (!$this->hasRequest($id, $token)) {
+                // Cancelled or replaced while the attachment was deferred.
+                continue;
+            }
+
+            $entry = $this->handles[$id];
+
+            try {
+                $this->addCurlHandle($entry['easy']);
+            } catch (\Throwable $e) {
+                // The promise has already escaped, so reject it rather than
+                // throw. User code may have settled it directly; a settled
+                // promise must not abort the rest of the snapshot.
+                $rejection = $this->discardPendingRequest($id, $entry, $e);
+                if (P\Is::pending($entry['deferred'])) {
+                    $entry['deferred']->reject($rejection);
+                }
+            }
         }
         }
     }
     }
 
 
     /**
     /**
      * Cancels a handle from sending and removes references to it.
      * Cancels a handle from sending and removes references to it.
      *
      *
-     * @param int $id Handle ID to cancel and remove.
+     * @param int         $id        Handle ID to cancel and remove.
+     * @param object|null $waitToken Identity token that must still match the
+     *                               entry when given.
      *
      *
      * @return bool True on success, false on failure.
      * @return bool True on success, false on failure.
      */
      */
-    private function cancel($id): bool
+    private function cancel($id, ?object $waitToken = null): bool
     {
     {
         if (!is_int($id)) {
         if (!is_int($id)) {
-            trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing an integer to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
+            \trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing an int to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
         }
         }
 
 
-        // Cannot cancel if it has been processed.
-        if (!isset($this->handles[$id])) {
+        // Cannot cancel if it has been processed or replaced by a request
+        // that reused the native handle ID.
+        if (!isset($this->handles[$id]) || ($waitToken !== null && ($this->handles[$id]['wait_token'] ?? null) !== $waitToken)) {
             return false;
             return false;
         }
         }
 
 
-        $handle = $this->handles[$id]['easy']->handle;
-        unset($this->delays[$id], $this->handles[$id]);
-        \curl_multi_remove_handle($this->_mh, $handle);
-        \curl_close($handle);
+        $entry = $this->handles[$id];
+        $easy = $entry['easy'];
+        $attached = !empty($entry['attached']);
+        unset($this->delays[$id], $this->deferredAdds[$id], $this->handles[$id]);
+
+        if ($this->multiExecDepth > 0) {
+            $this->deferredCancels[$id] = ['easy' => $easy, 'attached' => $attached];
+
+            return true;
+        }
+
+        $this->cleanupCancelledHandle($easy, $attached);
 
 
         return true;
         return true;
     }
     }
 
 
-    private function processMessages(): void
+    private function cleanupDeferredCancels(?\Throwable &$failure): void
     {
     {
-        while ($done = \curl_multi_info_read($this->_mh)) {
-            if ($done['msg'] !== \CURLMSG_DONE) {
-                // if it's not done, then it would be premature to remove the handle. ref https://github.com/guzzle/guzzle/pull/2892#issuecomment-945150216
-                continue;
+        if ($this->deferredCancels === []) {
+            return;
+        }
+
+        $entries = $this->deferredCancels;
+        $this->deferredCancels = [];
+
+        foreach ($entries as $entry) {
+            try {
+                $this->cleanupCancelledHandle($entry['easy'], $entry['attached']);
+            } catch (\Throwable $e) {
+                // A final progress update can run a throwing user callback;
+                // clean the remaining entries and surface the first failure
+                // once the drain completes.
+                if ($failure === null) {
+                    $failure = $e;
+                }
             }
             }
-            $id = (int) $done['handle'];
-            \curl_multi_remove_handle($this->_mh, $done['handle']);
+        }
+    }
 
 
-            if (!isset($this->handles[$id])) {
-                // Probably was cancelled.
-                continue;
+    private function cleanupCancelledHandle(EasyHandle $easy, bool $attached): void
+    {
+        $handle = $easy->handle;
+        $failure = null;
+
+        if ($attached) {
+            try {
+                $this->removeHandleFromMulti($handle);
+            } catch (\Throwable $e) {
+                // The native detach completes even when its final progress
+                // callback throws; finish this entry before rethrowing.
+                $failure = $e;
             }
             }
+        }
 
 
-            $entry = $this->handles[$id];
-            unset($this->handles[$id], $this->delays[$id]);
-            $entry['easy']->errno = $done['result'];
-            $entry['deferred']->resolve(
-                CurlFactory::finish($this, $entry['easy'], $this->factory)
-            );
+        $this->unmarkProxyTunnelActive($easy);
+
+        if (PHP_VERSION_ID < 80000) {
+            try {
+                \curl_close($handle);
+            } catch (\Throwable $e) {
+                // An error handler can promote the close warning; keep the
+                // first failure.
+                if ($failure === null) {
+                    $failure = $e;
+                }
+            }
+        }
+
+        if ($failure !== null) {
+            throw $failure;
         }
         }
     }
     }
 
 
-    private function timeToNext(): int
+    private function processMessages(): void
+    {
+        // CurlFactory::finish can retry a transfer by re-invoking this handler
+        // from inside this loop; the guard keeps that re-entry from recreating
+        // the multi handle mid-iteration (see applyProxyTunnelOwnership). A
+        // depth is tracked because a completion callback can re-enter tick(),
+        // and the nested frame must not clear the outer loop's guard.
+        ++$this->messageProcessingDepth;
+
+        try {
+            while ($done = \curl_multi_info_read($this->_mh)) {
+                if ($done['msg'] !== \CURLMSG_DONE) {
+                    // if it's not done, then it would be premature to remove the handle. ref https://github.com/guzzle/guzzle/pull/2892#issuecomment-945150216
+                    continue;
+                }
+                if (!isset($done['handle'])) {
+                    // Work around a PHP issue where cancelled transfers may omit the handle.
+                    // Remove this once we no longer support PHP versions before the fix in
+                    // https://github.com/php/php-src/pull/16302.
+                    continue;
+                }
+                $id = (int) $done['handle'];
+                $this->removeCompletedHandleFromMulti($id, $done['handle']);
+
+                if (!isset($this->handles[$id])) {
+                    // Probably was cancelled.
+                    continue;
+                }
+
+                $entry = $this->handles[$id];
+                unset($this->handles[$id], $this->delays[$id]);
+                $entry['easy']->errno = $done['result'];
+
+                // finish() can run completion callbacks that cancel this
+                // promise; a settled promise must not be settled again.
+                try {
+                    $result = CurlFactory::finish($this, $entry['easy'], $this->factory);
+                } catch (\Throwable $e) {
+                    if (P\Is::pending($entry['deferred'])) {
+                        $entry['deferred']->reject($e);
+                    }
+
+                    continue;
+                }
+
+                if (P\Is::pending($entry['deferred'])) {
+                    $entry['deferred']->resolve($result);
+                }
+            }
+        } finally {
+            --$this->messageProcessingDepth;
+        }
+    }
+
+    /**
+     * Bounds a blocking select by the earliest pending request delay so a
+     * delayed transfer becoming due does not wait out an unrelated
+     * transfer's full select timeout.
+     *
+     * @return float|int
+     */
+    private function effectiveSelectTimeout()
+    {
+        if ($this->delays === []) {
+            return $this->selectTimeout;
+        }
+
+        return \min($this->selectTimeout, $this->secondsToNext());
+    }
+
+    /**
+     * @return float Seconds until the earliest pending delay is due
+     */
+    private function secondsToNext(): float
     {
     {
         $currentTime = Utils::currentTime();
         $currentTime = Utils::currentTime();
-        $nextTime = \PHP_INT_MAX;
+        $nextTime = \PHP_FLOAT_MAX;
         foreach ($this->delays as $time) {
         foreach ($this->delays as $time) {
             if ($time < $nextTime) {
             if ($time < $nextTime) {
                 $nextTime = $time;
                 $nextTime = $time;
             }
             }
         }
         }
 
 
-        return ((int) \max(0, $nextTime - $currentTime)) * 1000000;
+        return \max(0.0, $nextTime - $currentTime);
+    }
+
+    private function timeToNext(): int
+    {
+        // PHP_INT_MAX first: min() then returns the int operand whenever the
+        // microseconds exceed it, so the cast never sees an oversized float.
+        return (int) \min(\PHP_INT_MAX, $this->secondsToNext() * 1000000);
     }
     }
 }
 }

+ 182 - 0
modules/boonex/xero/plugins/guzzlehttp/guzzle/src/Handler/CurlShareHandleState.php

@@ -0,0 +1,182 @@
+<?php
+
+namespace GuzzleHttp\Handler;
+
+use GuzzleHttp\TransportSharing;
+
+/**
+ * @internal
+ */
+final class CurlShareHandleState
+{
+    /**
+     * @var resource|\CurlShareHandle|null
+     */
+    public $handle;
+
+    /**
+     * @var string
+     */
+    public $mode;
+
+    /**
+     * @param resource|\CurlShareHandle|null $handle
+     */
+    private function __construct(string $mode, $handle)
+    {
+        $this->mode = $mode;
+        $this->handle = $handle;
+    }
+
+    /**
+     * @param mixed $sharing
+     */
+    public static function fromOption($sharing): ?self
+    {
+        if ($sharing instanceof self) {
+            return $sharing;
+        }
+
+        $mode = self::normalizeMode($sharing, 'transport_sharing');
+        if ($mode === TransportSharing::NONE) {
+            return null;
+        }
+
+        if ($mode === TransportSharing::HANDLER_PREFER) {
+            return self::createHandlerShareOrNull($mode);
+        }
+
+        return self::createHandlerShare($mode);
+    }
+
+    /**
+     * @param mixed $sharing
+     */
+    public static function normalizeMode($sharing, string $option): string
+    {
+        if ($sharing instanceof self) {
+            return $sharing->mode;
+        }
+
+        if ($sharing === null || $sharing === TransportSharing::NONE) {
+            return TransportSharing::NONE;
+        }
+
+        if ($sharing === TransportSharing::HANDLER_PREFER || $sharing === TransportSharing::HANDLER_REQUIRE) {
+            return $sharing;
+        }
+
+        throw new \InvalidArgumentException(\sprintf(
+            'The "%s" option must be null or a GuzzleHttp\\TransportSharing::* constant; received %s.',
+            $option,
+            \get_debug_type($sharing)
+        ));
+    }
+
+    public static function assertNoRequiredSharingCustomFactoryConflict(array $options, string $handlerName): void
+    {
+        if (!\array_key_exists('handle_factory', $options) || $options['handle_factory'] === null) {
+            return;
+        }
+
+        $mode = self::normalizeMode($options['transport_sharing'] ?? null, 'transport_sharing');
+        if ($mode !== TransportSharing::HANDLER_REQUIRE) {
+            return;
+        }
+
+        throw new \InvalidArgumentException(\sprintf(
+            'The "transport_sharing" %s option cannot require sharing with a custom "handle_factory" because Guzzle cannot ensure that the custom factory applies CURLOPT_SHARE.',
+            $handlerName
+        ));
+    }
+
+    private static function createHandlerShareOrNull(string $mode): ?self
+    {
+        try {
+            return self::createHandlerShare($mode);
+        } catch (\Throwable $e) {
+            return null;
+        }
+    }
+
+    private static function createHandlerShare(string $mode): self
+    {
+        if (!\function_exists('curl_share_init') || !\function_exists('curl_share_setopt')) {
+            throw new \InvalidArgumentException('The "transport_sharing" option requires cURL share support.');
+        }
+
+        self::requireCurlConstant('CURLOPT_SHARE');
+        $shareOption = self::requireCurlConstant('CURLSHOPT_SHARE');
+        $locks = self::handlerLocks($mode);
+        $handle = curl_share_init();
+
+        try {
+            foreach ($locks as $lock) {
+                try {
+                    $success = curl_share_setopt($handle, $shareOption, $lock);
+                } catch (\Throwable $e) {
+                    throw new \InvalidArgumentException('Unable to configure cURL share handle: '.$e->getMessage(), 0, $e);
+                }
+
+                if (!$success) {
+                    throw new \InvalidArgumentException(\sprintf('Unable to configure cURL share handle with lock data %d.', $lock));
+                }
+            }
+        } catch (\Throwable $e) {
+            self::closeHandlerShareHandleOnPhp7($handle);
+
+            throw $e;
+        }
+
+        return new self($mode, $handle);
+    }
+
+    /**
+     * @return int[]
+     */
+    private static function handlerLocks(string $mode): array
+    {
+        CurlVersion::ensureHandlerSharingSupported();
+
+        if ($mode === TransportSharing::HANDLER_REQUIRE) {
+            CurlVersion::ensureSslSessionSharingSupported();
+        }
+
+        $locks = [
+            self::requireCurlConstant('CURL_LOCK_DATA_DNS'),
+        ];
+
+        if (CurlVersion::supportsSslSessionSharing()) {
+            $locks[] = self::requireCurlConstant('CURL_LOCK_DATA_SSL_SESSION');
+        }
+
+        return $locks;
+    }
+
+    private static function requireCurlConstant(string $constant): int
+    {
+        if (!\defined($constant)) {
+            throw new \InvalidArgumentException(\sprintf(
+                'The "transport_sharing" option requires %s, but it is not available in the installed PHP cURL extension.',
+                $constant
+            ));
+        }
+
+        $value = \constant($constant);
+        if (!\is_int($value)) {
+            throw new \InvalidArgumentException(\sprintf('The cURL constant %s must resolve to an integer.', $constant));
+        }
+
+        return $value;
+    }
+
+    /**
+     * @param resource|\CurlShareHandle $handle
+     */
+    private static function closeHandlerShareHandleOnPhp7($handle): void
+    {
+        if (\PHP_VERSION_ID < 80000 && \is_resource($handle)) {
+            curl_share_close($handle);
+        }
+    }
+}

+ 322 - 0
modules/boonex/xero/plugins/guzzlehttp/guzzle/src/Handler/CurlVersion.php

@@ -0,0 +1,322 @@
+<?php
+
+namespace GuzzleHttp\Handler;
+
+/**
+ * @internal
+ */
+final class CurlVersion
+{
+    private const MIN_VERSION = '7.21.2';
+
+    private const TLS_12_VERSION = '7.34.0';
+
+    private const TLS_13_VERSION = '7.52.0';
+
+    private const CONNECTION_CAP_VERSION = '7.30.0';
+
+    // CURLOPT_PIPEWAIT exists since libcurl 7.43.0, and multi handles have
+    // multiplexed by default since 7.62.0 - but a 7.65.0-7.65.1 regression
+    // dropped that default, which 7.65.2 restored, so 7.65.2 is the floor at
+    // which PIPEWAIT is reliably effective.
+    private const MULTIPLEX_VERSION = '7.65.2';
+
+    // libcurl's connection matcher refuses to hand a transfer wanting
+    // HTTP/1.x a pooled connection that already negotiated HTTP/2 or newer
+    // from 7.77.0: ConnectionExists() in lib/url.c gained the check between
+    // the 7.76.0 and 7.77.0 releases. The HTTP/2 branch of the check
+    // regressed to a debug log in 8.11.0 (curl commit 433d730) and was
+    // restored in 8.13.0 via the negotiation mask (curl commit db72b8d), so
+    // 8.11.0 through 8.12.1 are vulnerable again.
+    private const HTTP_VERSION_REUSE_MATCH_VERSION = '7.77.0';
+
+    private const HTTP_VERSION_REUSE_MATCH_REGRESSION = '8.11.0';
+
+    private const HTTP_VERSION_REUSE_MATCH_RESTORED = '8.13.0';
+
+    // CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE restricts the ALPN offer to h2 only
+    // since libcurl 8.10.0, and connection reuse matching stopped handing
+    // lower-version connections to prior-knowledge transfers in 8.14.0; below
+    // that, a required request could silently be sent over a reused HTTP/1.1
+    // connection.
+    private const REQUIRED_MULTIPLEX_VERSION = '8.14.0';
+
+    // curl 7.52.0 introduced HTTPS proxy support, advertised by a feature bit
+    // (a build can meet the version yet lack the feature). Earlier libcurl
+    // mishandles an https:// proxy: before 7.50.2 it silently downgrades to a
+    // plaintext HTTP proxy, and 7.50.2 through 7.51 reject it at connect time.
+    private const HTTPS_PROXY_VERSION = '7.52.0';
+
+    private const HANDLER_SHARING_VERSION = '7.35.0';
+
+    private const SSL_SESSION_SHARING_VERSION = '8.6.0';
+
+    // curl 7.57.0 added share-handle connection caches through
+    // CURL_LOCK_DATA_CONNECT; older share objects can only hold DNS, TLS
+    // session, and cookie data, never connections.
+    private const SHARE_CONNECTION_CACHE_VERSION = '7.57.0';
+
+    // curl 7.83.1 added proxy TLS-SRP to the connection-reuse match
+    // (CVE-2022-27782); the proxy client certificate was matched from 7.52.0,
+    // so proxy TLS credentials are trusted from 7.83.1 onwards.
+    private const PROXY_TLS_CREDENTIAL_REUSE_VERSION = '7.83.1';
+
+    // curl 8.19.0 fixed proxy tunnel reuse after credential changes
+    // (CVE-2026-3784), but related proxy credential leak flaws were only
+    // fixed in 8.20.0, so connection reuse is trusted from 8.20.0 onwards.
+    private const PROXY_CREDENTIAL_REUSE_VERSION = '8.20.0';
+
+    // curl 7.69.0 started comparing SOCKS proxy credentials when matching
+    // connections for reuse (curl #4835); older libcurl matches a SOCKS proxy
+    // by type, host, and port only.
+    private const SOCKS_PROXY_CREDENTIAL_REUSE_VERSION = '7.69.0';
+
+    private const PROXY_HEADER_SEPARATION_VERSION = '7.37.0';
+
+    /**
+     * @var array{version: string, features: int}|false|null
+     */
+    private static $versionInfo;
+
+    private function __construct()
+    {
+    }
+
+    public static function supportsCurlHandler(): bool
+    {
+        $version = self::getVersion();
+
+        return $version !== null && \version_compare($version, self::MIN_VERSION, '>=');
+    }
+
+    public static function supportsTls12(): bool
+    {
+        $version = self::getVersion();
+
+        return self::supportsSsl()
+            && \defined('CURL_SSLVERSION_TLSv1_2')
+            && $version !== null
+            && \version_compare($version, self::TLS_12_VERSION, '>=');
+    }
+
+    public static function supportsTls13(): bool
+    {
+        $version = self::getVersion();
+
+        return self::supportsSsl()
+            && \defined('CURL_SSLVERSION_TLSv1_3')
+            && $version !== null
+            && \version_compare($version, self::TLS_13_VERSION, '>=');
+    }
+
+    public static function supportsHttp2(): bool
+    {
+        $versionInfo = self::getVersionInfo();
+
+        return self::supportsTls12()
+            && \defined('CURL_VERSION_HTTP2')
+            && $versionInfo !== null
+            && 0 !== (\CURL_VERSION_HTTP2 & $versionInfo['features']);
+    }
+
+    public static function supportsMultiplex(): bool
+    {
+        $version = self::getVersion();
+
+        return \defined('CURLOPT_PIPEWAIT')
+            && $version !== null
+            && \version_compare($version, self::MULTIPLEX_VERSION, '>=');
+    }
+
+    public static function supportsHttpVersionReuseMatching(): bool
+    {
+        $version = self::getVersion();
+
+        if ($version === null || \version_compare($version, self::HTTP_VERSION_REUSE_MATCH_VERSION, '<')) {
+            return false;
+        }
+
+        return \version_compare($version, self::HTTP_VERSION_REUSE_MATCH_REGRESSION, '<')
+            || \version_compare($version, self::HTTP_VERSION_REUSE_MATCH_RESTORED, '>=');
+    }
+
+    public static function supportsConnectionCaps(): bool
+    {
+        $version = self::getVersion();
+
+        return \defined('CURLMOPT_MAX_HOST_CONNECTIONS')
+            && \defined('CURLMOPT_MAX_TOTAL_CONNECTIONS')
+            && $version !== null
+            && \version_compare($version, self::CONNECTION_CAP_VERSION, '>=');
+    }
+
+    public static function ensureConnectionCapsSupported(string $option): void
+    {
+        if (self::supportsConnectionCaps()) {
+            return;
+        }
+
+        throw new \InvalidArgumentException(\sprintf(
+            'The "%s" option requires PHP cURL support for CURLMOPT_MAX_HOST_CONNECTIONS and CURLMOPT_MAX_TOTAL_CONNECTIONS with libcurl %s or newer.',
+            $option,
+            self::CONNECTION_CAP_VERSION
+        ));
+    }
+
+    public static function supportsRequiredMultiplex(): bool
+    {
+        $version = self::getVersion();
+
+        return \defined('CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE')
+            && $version !== null
+            && self::supportsHttp2()
+            && \version_compare($version, self::REQUIRED_MULTIPLEX_VERSION, '>=');
+    }
+
+    public static function supportsHttpsProxy(): bool
+    {
+        $versionInfo = self::getVersionInfo();
+
+        // CURL_VERSION_HTTPS_PROXY is not defined on every supported PHP
+        // version; fall back to the curl.h bit value.
+        $httpsProxyFeature = \defined('CURL_VERSION_HTTPS_PROXY') ? \CURL_VERSION_HTTPS_PROXY : (1 << 21);
+
+        return $versionInfo !== null
+            && \version_compare($versionInfo['version'], self::HTTPS_PROXY_VERSION, '>=')
+            && 0 !== ($httpsProxyFeature & $versionInfo['features']);
+    }
+
+    public static function supportsNtlm(): bool
+    {
+        $versionInfo = self::getVersionInfo();
+
+        // CURL_VERSION_NTLM is not defined on every supported PHP version; fall
+        // back to the curl.h bit value.
+        $ntlmFeature = \defined('CURL_VERSION_NTLM') ? \CURL_VERSION_NTLM : (1 << 4);
+
+        return \defined('CURLAUTH_NTLM')
+            && $versionInfo !== null
+            && 0 !== ($ntlmFeature & $versionInfo['features']);
+    }
+
+    public static function supportsHandlerSharing(): bool
+    {
+        $version = self::getVersion();
+
+        return $version !== null && \version_compare($version, self::HANDLER_SHARING_VERSION, '>=');
+    }
+
+    public static function ensureHandlerSharingSupported(): void
+    {
+        if (!self::supportsHandlerSharing()) {
+            throw new \InvalidArgumentException(\sprintf(
+                'The "transport_sharing" option requires libcurl %s or higher for cURL share handles.',
+                self::HANDLER_SHARING_VERSION
+            ));
+        }
+    }
+
+    public static function supportsSslSessionSharing(): bool
+    {
+        $version = self::getVersion();
+
+        return self::supportsSsl()
+            && $version !== null
+            && \version_compare($version, self::SSL_SESSION_SHARING_VERSION, '>=');
+    }
+
+    public static function ensureSslSessionSharingSupported(): void
+    {
+        if (!self::supportsSslSessionSharing()) {
+            throw new \InvalidArgumentException(\sprintf(
+                'The "transport_sharing" option requires libcurl %s or higher with SSL support for SSL session sharing.',
+                self::SSL_SESSION_SHARING_VERSION
+            ));
+        }
+    }
+
+    public static function supportsShareConnectionCaches(): bool
+    {
+        $version = self::getVersion();
+
+        // An undetectable libcurl version is treated as capable so the
+        // opaque share safeguards fail closed.
+        return $version === null || \version_compare($version, self::SHARE_CONNECTION_CACHE_VERSION, '>=');
+    }
+
+    public static function supportsProxyTlsCredentialAwareConnectionReuse(): bool
+    {
+        $version = self::getVersion();
+
+        return $version !== null
+            && \version_compare($version, self::PROXY_TLS_CREDENTIAL_REUSE_VERSION, '>=');
+    }
+
+    public static function supportsProxyCredentialAwareConnectionReuse(): bool
+    {
+        $version = self::getVersion();
+
+        return $version !== null
+            && \version_compare($version, self::PROXY_CREDENTIAL_REUSE_VERSION, '>=');
+    }
+
+    public static function supportsSocksProxyCredentialAwareConnectionReuse(): bool
+    {
+        $version = self::getVersion();
+
+        return $version !== null
+            && \version_compare($version, self::SOCKS_PROXY_CREDENTIAL_REUSE_VERSION, '>=');
+    }
+
+    public static function supportsProxyHeaderSeparation(): bool
+    {
+        $version = self::getVersion();
+
+        return $version !== null
+            && \version_compare($version, self::PROXY_HEADER_SEPARATION_VERSION, '>=')
+            && \defined('CURLOPT_PROXYHEADER')
+            && \defined('CURLOPT_HEADEROPT')
+            && \defined('CURLHEADER_SEPARATE');
+    }
+
+    private static function supportsSsl(): bool
+    {
+        $versionInfo = self::getVersionInfo();
+
+        return \defined('CURL_VERSION_SSL')
+            && $versionInfo !== null
+            && 0 !== (\CURL_VERSION_SSL & $versionInfo['features']);
+    }
+
+    public static function getVersion(): ?string
+    {
+        $versionInfo = self::getVersionInfo();
+
+        return $versionInfo === null ? null : $versionInfo['version'];
+    }
+
+    /**
+     * @return array{version: string, features: int}|null
+     */
+    private static function getVersionInfo(): ?array
+    {
+        if (self::$versionInfo === null) {
+            if (!\function_exists('curl_version')) {
+                self::$versionInfo = false;
+            } else {
+                $versionInfo = \curl_version();
+                self::$versionInfo = \is_array($versionInfo)
+                    && isset($versionInfo['version'], $versionInfo['features'])
+                    && \is_string($versionInfo['version'])
+                    && \is_int($versionInfo['features'])
+                        ? [
+                            'version' => $versionInfo['version'],
+                            'features' => $versionInfo['features'],
+                        ]
+                        : false;
+            }
+        }
+
+        return self::$versionInfo === false ? null : self::$versionInfo;
+    }
+}

+ 29 - 3
modules/boonex/xero/plugins/guzzlehttp/guzzle/src/Handler/EasyHandle.php

@@ -30,6 +30,17 @@ final class EasyHandle
      */
      */
     public $headers = [];
     public $headers = [];
 
 
+    /**
+     * @var array Valid trailer lines, retained only when an on_trailers
+     *            callback is configured
+     */
+    public $trailers = [];
+
+    /**
+     * @var bool Whether this handle was configured with CURLOPT_PIPEWAIT
+     */
+    public $usesPipewait = false;
+
     /**
     /**
      * @var ResponseInterface|null Received response (if any)
      * @var ResponseInterface|null Received response (if any)
      */
      */
@@ -50,13 +61,26 @@ final class EasyHandle
      */
      */
     public $errno = 0;
     public $errno = 0;
 
 
+    /**
+     * @var string|null Effective CURLOPT_PROXY value the handle was created with (if any)
+     */
+    public $effectiveProxy;
+
+    /**
+     * Proxy tunnel or SOCKS proxy section signature for connection-reuse
+     * isolation, or null when the request does not require sectioning.
+     *
+     * @var string|null
+     */
+    public $proxyTunnelSignature;
+
     /**
     /**
      * @var \Throwable|null Exception during on_headers (if any)
      * @var \Throwable|null Exception during on_headers (if any)
      */
      */
     public $onHeadersException;
     public $onHeadersException;
 
 
     /**
     /**
-     * @var \Exception|null Exception during createResponse (if any)
+     * @var \Throwable|null Exception during createResponse (if any)
      */
      */
     public $createResponseException;
     public $createResponseException;
 
 
@@ -68,11 +92,13 @@ final class EasyHandle
      */
      */
     public function createResponse(): void
     public function createResponse(): void
     {
     {
+        $this->response = null;
+
         [$ver, $status, $reason, $headers] = HeaderProcessor::parseHeaders($this->headers);
         [$ver, $status, $reason, $headers] = HeaderProcessor::parseHeaders($this->headers);
 
 
         $normalizedKeys = Utils::normalizeHeaderKeys($headers);
         $normalizedKeys = Utils::normalizeHeaderKeys($headers);
 
 
-        if (!empty($this->options['decode_content']) && isset($normalizedKeys['content-encoding'])) {
+        if (isset($this->options['decode_content']) && $this->options['decode_content'] !== false && isset($normalizedKeys['content-encoding'])) {
             $headers['x-encoded-content-encoding'] = $headers[$normalizedKeys['content-encoding']];
             $headers['x-encoded-content-encoding'] = $headers[$normalizedKeys['content-encoding']];
             unset($headers[$normalizedKeys['content-encoding']]);
             unset($headers[$normalizedKeys['content-encoding']]);
             if (isset($normalizedKeys['content-length'])) {
             if (isset($normalizedKeys['content-length'])) {
@@ -80,7 +106,7 @@ final class EasyHandle
 
 
                 $bodyLength = (int) $this->sink->getSize();
                 $bodyLength = (int) $this->sink->getSize();
                 if ($bodyLength) {
                 if ($bodyLength) {
-                    $headers[$normalizedKeys['content-length']] = $bodyLength;
+                    $headers[$normalizedKeys['content-length']] = [(string) $bodyLength];
                 } else {
                 } else {
                     unset($headers[$normalizedKeys['content-length']]);
                     unset($headers[$normalizedKeys['content-length']]);
                 }
                 }

+ 56 - 1
modules/boonex/xero/plugins/guzzlehttp/guzzle/src/Handler/HeaderProcessor.php

@@ -24,7 +24,14 @@ final class HeaderProcessor
             throw new \RuntimeException('Expected a non-empty array of header data');
             throw new \RuntimeException('Expected a non-empty array of header data');
         }
         }
 
 
-        $parts = \explode(' ', \array_shift($headers), 3);
+        $headers = self::getLastHeaderBlock(\array_values($headers));
+
+        $statusLine = \array_shift($headers);
+        if ($statusLine === null) {
+            throw new \RuntimeException('Expected a non-empty array of header data');
+        }
+
+        $parts = \explode(' ', $statusLine, 3);
         $version = \explode('/', $parts[0])[1] ?? null;
         $version = \explode('/', $parts[0])[1] ?? null;
 
 
         if ($version === null) {
         if ($version === null) {
@@ -37,6 +44,54 @@ final class HeaderProcessor
             throw new \RuntimeException('HTTP status code missing from header data');
             throw new \RuntimeException('HTTP status code missing from header data');
         }
         }
 
 
+        if (!\preg_match('/^\d{3}$/D', $status)) {
+            throw new \RuntimeException('HTTP status code is invalid');
+        }
+
+        foreach ($headers as $header) {
+            if (\strpos($header, ':') === false) {
+                throw new \RuntimeException('HTTP header line is invalid');
+            }
+        }
+
         return [$version, (int) $status, $parts[2] ?? null, Utils::headersFromLines($headers)];
         return [$version, (int) $status, $parts[2] ?? null, Utils::headersFromLines($headers)];
     }
     }
+
+    public static function isStatusLineCandidate(string $line): bool
+    {
+        return \preg_match('/^HTTP\/[0-9]+(?:\.[0-9]+)? [0-9]{3}(?: [^\r\n]*)?(?:\r\n|\r|\n)?$/iD', $line) === 1;
+    }
+
+    public static function isValidHeaderFieldLine(string $line): bool
+    {
+        $parts = \explode(':', $line, 2);
+
+        if (!isset($parts[1])) {
+            return false;
+        }
+
+        if (!\preg_match('/^[a-zA-Z0-9\'`#$%&*+.^_|~!-]+$/D', $parts[0])) {
+            return false;
+        }
+
+        return \preg_match('/^[\x20\x09\x21-\x7E\x80-\xFF]*(?:\r\n|\r|\n)?$/D', \trim($parts[1], " \t")) === 1;
+    }
+
+    /**
+     * @param non-empty-list<string> $headers
+     *
+     * @return list<string>
+     */
+    private static function getLastHeaderBlock(array $headers): array
+    {
+        $lastStatusLine = 0;
+
+        foreach ($headers as $index => $line) {
+            if (self::isStatusLineCandidate($line)) {
+                $lastStatusLine = $index;
+            }
+        }
+
+        return \array_slice($headers, $lastStatusLine);
+    }
 }
 }

+ 198 - 0
modules/boonex/xero/plugins/guzzlehttp/guzzle/src/Handler/HostValidator.php

@@ -0,0 +1,198 @@
+<?php
+
+namespace GuzzleHttp\Handler;
+
+use GuzzleHttp\Exception\RequestException;
+use Psr\Http\Message\RequestInterface;
+
+/**
+ * Rejects request hosts that a handler could resolve differently from the host
+ * the request names.
+ *
+ * @internal
+ */
+final class HostValidator
+{
+    private function __construct()
+    {
+    }
+
+    /**
+     * Asserts that a request names one unambiguous network host.
+     *
+     * The URI host and every Host header value must use printable ASCII without
+     * percent escapes. The URI host must also exclude authority delimiters and
+     * numeric-looking parts followed by trailing dots. Handlers reparse the URI
+     * but send the Host header as given, so ambiguous spellings can name
+     * different connection and request hosts.
+     *
+     * @throws RequestException
+     */
+    public static function assertRequestHost(RequestInterface $request): void
+    {
+        $host = $request->getUri()->getHost();
+
+        self::assertUriHostValue($host, $request);
+        self::assertNoAuthorityDelimiter($host, $request);
+        self::assertNotADottedAddress($host, $request);
+
+        foreach ($request->getHeader('Host') as $value) {
+            self::assertHostHeaderValue((string) $value, $request);
+        }
+    }
+
+    /**
+     * @throws RequestException
+     */
+    private static function assertUriHostValue(string $value, RequestInterface $request): void
+    {
+        if (!self::isPrintableAscii($value)) {
+            throw new RequestException(\sprintf('The request URI host "%s" must contain only printable ASCII characters, because a handler can otherwise connect to a host that differs from the one the request names. An internationalized host name has an A-label form that this rule accepts.', self::escape($value)), $request);
+        }
+
+        if (\strpos($value, '%') !== false) {
+            throw new RequestException(\sprintf('The request URI host "%s" must not contain a percent escape, because a handler can decode it and then connect to a host that differs from the one the request names.', self::escape($value)), $request);
+        }
+    }
+
+    /**
+     * The Host header is sent rather than reparsed for the connection, so its
+     * diagnostics describe a request authority the caller did not write.
+     *
+     * @throws RequestException
+     */
+    private static function assertHostHeaderValue(string $value, RequestInterface $request): void
+    {
+        if (!self::isPrintableAscii($value)) {
+            throw new RequestException(\sprintf('The request Host header "%s" must contain only printable ASCII characters, because an intermediary or an origin server can otherwise read it as an authority that differs from the one the request names. An internationalized host name has an A-label form that this rule accepts.', self::escape($value)), $request);
+        }
+
+        if (\strpos($value, '%') !== false) {
+            throw new RequestException(\sprintf('The request Host header "%s" must not contain a percent escape, because an intermediary or an origin server can decode it and then read it as an authority that differs from the one the request names.', self::escape($value)), $request);
+        }
+    }
+
+    /**
+     * Matches the accepted shape positively so a PCRE failure rejects.
+     */
+    private static function isPrintableAscii(string $value): bool
+    {
+        return \preg_match('/\A[\x21-\x7E]*\z/D', $value) === 1;
+    }
+
+    /**
+     * Rejects a delimiter the transport could treat as the end of the URI host.
+     *
+     * This mirrors GuzzleHttp\Psr7\Uri::assertValidHost() and only affects
+     * third-party UriInterface values. Host headers may carry a port and are
+     * sent verbatim.
+     *
+     * @throws RequestException
+     */
+    private static function assertNoAuthorityDelimiter(string $host, RequestInterface $request): void
+    {
+        $message = 'The request URI host "%s" must not contain a URI authority delimiter, because a handler reparses the URI and can then connect to a host that differs from the one the request names.';
+
+        // Match the accepted shape positively so a PCRE engine failure rejects.
+        if (\preg_match('/\A[^\/?#@\\\\]*\z/D', $host) !== 1) {
+            throw new RequestException(\sprintf($message, self::escape($host)), $request);
+        }
+
+        if (\strpos($host, '[') !== false || \strpos($host, ']') !== false) {
+            if (\strpos($host, '[') !== 0 || \substr($host, -1) !== ']') {
+                throw new RequestException(\sprintf($message, self::escape($host)), $request);
+            }
+
+            return;
+        }
+
+        if (\strpos($host, ':') !== false) {
+            throw new RequestException(\sprintf($message, self::escape($host)), $request);
+        }
+    }
+
+    /**
+     * Rejects one to four numeric-looking parts followed by trailing dots.
+     *
+     * libcurl 8.21.0 drops a trailing dot from inet_aton-style numeric hosts
+     * before connecting, while other validators treat the input as a name.
+     * Testing the shape also rejects some out-of-range values that transports
+     * keep as names; isNumericIpv4Host() explains that fail-closed tradeoff.
+     * Plain numeric shorthand stays accepted.
+     *
+     * @throws RequestException
+     */
+    private static function assertNotADottedAddress(string $host, RequestInterface $request): void
+    {
+        if (\substr($host, -1) !== '.') {
+            return;
+        }
+
+        if (!self::isNumericIpv4Host(\rtrim($host, '.'))) {
+            return;
+        }
+
+        throw new RequestException(\sprintf('The request URI host "%s" must not be written as one to four decimal, octal or hexadecimal parts followed by one or more trailing dots, because a handler can read that spelling as an IPv4 address and connect to that address while the rest of the process reads a name.', self::escape($host)), $request);
+    }
+
+    /**
+     * Reports whether a value has the transport's inet_aton-style shape: one
+     * to four decimal, 0-prefixed octal, or 0x-prefixed hexadecimal parts.
+     *
+     * Range and 32-bit overflow checks are deliberately omitted. This may
+     * reject a trailing-dot spelling the transport reads as a name, but avoids
+     * missing one it resolves as an address. No PCRE is used.
+     */
+    public static function isNumericIpv4Host(string $host): bool
+    {
+        if ($host === '') {
+            return false;
+        }
+
+        $parts = \explode('.', $host);
+
+        if (\count($parts) > 4) {
+            return false;
+        }
+
+        foreach ($parts as $part) {
+            if (!self::isNumericIpv4Part($part)) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    private static function isNumericIpv4Part(string $part): bool
+    {
+        if ($part === '') {
+            return false;
+        }
+
+        if ($part[0] === '0' && isset($part[1]) && ($part[1] === 'x' || $part[1] === 'X')) {
+            return \strlen($part) > 2 && \strspn($part, '0123456789abcdefABCDEF', 2) === \strlen($part) - 2;
+        }
+
+        $digits = $part[0] === '0' ? '01234567' : '0123456789';
+
+        return \strspn($part, $digits) === \strlen($part);
+    }
+
+    /**
+     * Escapes non-printable bytes as uppercase \xNN for safe diagnostics.
+     * Printable delimiters and dots stay visible. The result is not a
+     * reversible encoding.
+     */
+    private static function escape(string $value): string
+    {
+        $escaped = '';
+
+        for ($offset = 0, $length = \strlen($value); $offset < $length; ++$offset) {
+            $byte = \ord($value[$offset]);
+            $escaped .= $byte >= 0x21 && $byte <= 0x7E ? $value[$offset] : \sprintf('\\x%02X', $byte);
+        }
+
+        return $escaped;
+    }
+}

+ 1 - 2
modules/boonex/xero/plugins/guzzlehttp/guzzle/src/Handler/MockHandler.php

@@ -7,7 +7,6 @@ use GuzzleHttp\HandlerStack;
 use GuzzleHttp\Promise as P;
 use GuzzleHttp\Promise as P;
 use GuzzleHttp\Promise\PromiseInterface;
 use GuzzleHttp\Promise\PromiseInterface;
 use GuzzleHttp\TransferStats;
 use GuzzleHttp\TransferStats;
-use GuzzleHttp\Utils;
 use Psr\Http\Message\RequestInterface;
 use Psr\Http\Message\RequestInterface;
 use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\StreamInterface;
 use Psr\Http\Message\StreamInterface;
@@ -160,7 +159,7 @@ class MockHandler implements \Countable
             ) {
             ) {
                 $this->queue[] = $value;
                 $this->queue[] = $value;
             } else {
             } else {
-                throw new \TypeError('Expected a Response, Promise, Throwable or callable. Found '.Utils::describeType($value));
+                throw new \TypeError('Expected a Response, Promise, Throwable or callable. Found '.\get_debug_type($value));
             }
             }
         }
         }
     }
     }

+ 30 - 0
modules/boonex/xero/plugins/guzzlehttp/guzzle/src/Handler/Proxy.php

@@ -48,4 +48,34 @@ class Proxy
             return empty($options['stream']) ? $default($request, $options) : $streaming($request, $options);
             return empty($options['stream']) ? $default($request, $options) : $streaming($request, $options);
         };
         };
     }
     }
+
+    /**
+     * Sends requests to a fallback handler when the default cURL handler cannot
+     * honor TLS 1.2 selection.
+     *
+     * @param callable(RequestInterface, array): PromiseInterface $default
+     * @param callable(RequestInterface, array): PromiseInterface $fallback
+     *
+     * @return callable(RequestInterface, array): PromiseInterface Returns the composed handler.
+     */
+    public static function wrapTlsFallback(callable $default, callable $fallback): callable
+    {
+        return static function (RequestInterface $request, array $options) use ($default, $fallback): PromiseInterface {
+            if (self::requiresTls12Fallback($options)) {
+                return $fallback($request, $options);
+            }
+
+            return $default($request, $options);
+        };
+    }
+
+    /**
+     * @param array<string, mixed> $options
+     */
+    private static function requiresTls12Fallback(array $options): bool
+    {
+        return isset($options[RequestOptions::CRYPTO_METHOD])
+            && $options[RequestOptions::CRYPTO_METHOD] === \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT
+            && !CurlVersion::supportsTls12();
+    }
 }
 }

+ 121 - 0
modules/boonex/xero/plugins/guzzlehttp/guzzle/src/Handler/ProxyEnvironment.php

@@ -0,0 +1,121 @@
+<?php
+
+namespace GuzzleHttp\Handler;
+
+use GuzzleHttp\Psr7;
+
+/**
+ * Resolves proxy configuration from the process environment with the same
+ * semantics libcurl applies, so the cURL handlers can pin CURLOPT_PROXY and
+ * CURLOPT_NOPROXY explicitly and libcurl never reads the environment itself.
+ *
+ * @internal
+ */
+final class ProxyEnvironment
+{
+    private function __construct()
+    {
+    }
+
+    /**
+     * Resolves the proxy to use for the given request scheme.
+     *
+     * The lookup mirrors libcurl for the http and https schemes the handlers
+     * accept: the lowercase scheme-specific variable first, its uppercase
+     * variant next (except for "http", where uppercase HTTP_PROXY is never
+     * read), then all_proxy/ALL_PROXY.
+     *
+     * @return string|null The proxy to use; null when the environment
+     *                     configures none.
+     */
+    public static function getProxyForScheme(string $scheme): ?string
+    {
+        $scheme = Psr7\Utils::asciiToLower($scheme);
+        $candidates = [$scheme.'_proxy'];
+        if ($scheme !== 'http') {
+            // Uppercase HTTP_PROXY is deliberately never consulted: a CGI
+            // request header "Proxy:" becomes HTTP_PROXY in the environment.
+            // See https://httpoxy.org for more information.
+            $candidates[] = Psr7\Utils::asciiToUpper($scheme).'_PROXY';
+        }
+        $candidates[] = 'all_proxy';
+        $candidates[] = 'ALL_PROXY';
+
+        foreach ($candidates as $name) {
+            $value = self::getenv($name);
+            if ($value !== null) {
+                return $value;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * @return string|null The no-proxy list; null when nothing is set.
+     */
+    public static function getNoProxy(): ?string
+    {
+        foreach (['no_proxy', 'NO_PROXY'] as $name) {
+            $value = self::getenv($name);
+            if ($value !== null) {
+                return $value;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Splits a no_proxy environment value into matchable entries.
+     *
+     * Mirrors libcurl's tokenization: entries may be separated by commas or
+     * blanks, and a single leading dot is ignored, so ".example.com" bypasses
+     * example.com and its subdomains exactly as a bare domain entry does.
+     *
+     * @return string[]
+     */
+    public static function splitNoProxy(string $noProxy): array
+    {
+        $entries = [];
+
+        $split = \preg_split('/[\s,]+/', $noProxy);
+
+        if ($split === false) {
+            throw new \RuntimeException('Unable to split the no_proxy value: '.\preg_last_error_msg());
+        }
+
+        foreach ($split as $entry) {
+            if ($entry !== '' && $entry[0] === '.') {
+                $entry = \substr($entry, 1);
+            }
+
+            if ($entry !== '') {
+                $entries[] = $entry;
+            }
+        }
+
+        return $entries;
+    }
+
+    private static function getenv(string $name): ?string
+    {
+        // Windows environment variables are case-insensitive, so the
+        // lowercase-only httpoxy defence does not hold there. Outside the
+        // CLI SAPI on Windows, environment proxies are not resolved at all
+        // (a safe-side divergence from libcurl).
+        if (\PHP_OS_FAMILY === 'Windows' && \PHP_SAPI !== 'cli') {
+            return null;
+        }
+
+        // local_only: the OS environment and putenv() only - the same
+        // environ(7) libcurl reads. SAPI request environments such as
+        // fastcgi_param or SetEnv are deliberately excluded.
+        $value = \getenv($name, true);
+
+        // libcurl's GetEnv (lib/getenv.c) treats variables set to an empty
+        // string as unset on every version, so the lookup falls through to
+        // the next candidate.
+        return $value === false || $value === '' ? null : $value;
+    }
+}

+ 681 - 57
modules/boonex/xero/plugins/guzzlehttp/guzzle/src/Handler/StreamHandler.php

@@ -4,11 +4,14 @@ namespace GuzzleHttp\Handler;
 
 
 use GuzzleHttp\Exception\ConnectException;
 use GuzzleHttp\Exception\ConnectException;
 use GuzzleHttp\Exception\RequestException;
 use GuzzleHttp\Exception\RequestException;
+use GuzzleHttp\Exception\TransferException;
+use GuzzleHttp\Multiplexing;
 use GuzzleHttp\Promise as P;
 use GuzzleHttp\Promise as P;
 use GuzzleHttp\Promise\FulfilledPromise;
 use GuzzleHttp\Promise\FulfilledPromise;
 use GuzzleHttp\Promise\PromiseInterface;
 use GuzzleHttp\Promise\PromiseInterface;
 use GuzzleHttp\Psr7;
 use GuzzleHttp\Psr7;
 use GuzzleHttp\TransferStats;
 use GuzzleHttp\TransferStats;
+use GuzzleHttp\TransportSharing;
 use GuzzleHttp\Utils;
 use GuzzleHttp\Utils;
 use Psr\Http\Message\RequestInterface;
 use Psr\Http\Message\RequestInterface;
 use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ResponseInterface;
@@ -22,11 +25,90 @@ use Psr\Http\Message\UriInterface;
  */
  */
 class StreamHandler
 class StreamHandler
 {
 {
+    private const KNOWN_CONSTRUCTOR_OPTIONS = [
+        'max_host_connections' => true,
+        'max_total_connections' => true,
+        'transport_sharing' => true,
+    ];
+
+    private const CONNECTION_ERRORS = [
+        'php_network_getaddresses:',
+        'getaddrinfo',
+        'gethostbyname failed',
+        'Connection refused',
+        'No connection could be made because the target machine actively refused it',
+        "couldn't connect to host", // error on HHVM
+        'connection attempt failed',
+        'connect() failed',
+        'Connection timed out',
+        'Operation timed out',
+        'Network is unreachable',
+        'No route to host',
+        'Host is unreachable',
+        'Host is down',
+        'Cannot connect to HTTPS server through proxy',
+    ];
+
     /**
     /**
      * @var array
      * @var array
      */
      */
     private $lastHeaders = [];
     private $lastHeaders = [];
 
 
+    /**
+     * @var string
+     */
+    private $transportSharingMode;
+
+    /**
+     * @var bool
+     */
+    private $connectionCapsConfigured = false;
+
+    /**
+     * Accepts an associative array of options:
+     *
+     * - max_host_connections: Optional positive integer or null. A non-null
+     *   value marks the handler as incompatible with enabled response
+     *   streaming; the number is not used for stream-handler admission.
+     * - max_total_connections: Optional positive integer or null. A non-null
+     *   value marks the handler as incompatible with enabled response
+     *   streaming; the number is not used for stream-handler admission.
+     * - transport_sharing: Optional transport sharing mode.
+     *
+     * The stream handler cannot cap streamed connections, so a configured cap
+     * marker rejects enabled response streaming ("stream" => true). Accepted
+     * transfers are buffered and hold at most one connection per in-flight
+     * call, but overlapping buffered calls are not collectively limited.
+     *
+     * @param array{max_host_connections?: mixed, max_total_connections?: mixed, transport_sharing?: mixed} $options Array of options to use with the handler
+     */
+    public function __construct(array $options = [])
+    {
+        foreach ($options as $name => $_) {
+            if (!isset(self::KNOWN_CONSTRUCTOR_OPTIONS[$name])) {
+                \trigger_deprecation('guzzlehttp/guzzle', '7.14', \sprintf('The "%s" StreamHandler constructor option is unknown; guzzlehttp/guzzle 8.0 will reject unknown constructor options.', (string) $name));
+            }
+        }
+
+        $this->transportSharingMode = CurlShareHandleState::normalizeMode(
+            $options['transport_sharing'] ?? null,
+            'transport_sharing'
+        );
+
+        foreach (['max_host_connections', 'max_total_connections'] as $capOption) {
+            $value = $options[$capOption] ?? null;
+            if ($value === null) {
+                continue;
+            }
+
+            if (!\is_int($value) || $value < 1) {
+                throw new \InvalidArgumentException(\sprintf('%s must be a positive integer.', $capOption));
+            }
+
+            $this->connectionCapsConfigured = true;
+        }
+    }
+
     /**
     /**
      * Sends an HTTP request.
      * Sends an HTTP request.
      *
      *
@@ -40,14 +122,47 @@ class StreamHandler
             \usleep($options['delay'] * 1000);
             \usleep($options['delay'] * 1000);
         }
         }
 
 
+        $multiplex = $options['multiplex'] ?? null;
+
+        // Multiplexing::NONE is trivially satisfied: the stream handler sends
+        // one HTTP/1.x request per connection and never multiplexes.
+        if (null !== $multiplex && !\in_array($multiplex, [Multiplexing::NONE, Multiplexing::EAGER, Multiplexing::WAIT, Multiplexing::REQUIRE_EAGER, Multiplexing::REQUIRE_WAIT], true)) {
+            throw new \InvalidArgumentException(\sprintf(
+                'The "multiplex" option must be null or a GuzzleHttp\\Multiplexing::* constant; received %s.',
+                \get_debug_type($multiplex)
+            ));
+        }
+
+        if (\in_array($multiplex, [Multiplexing::REQUIRE_EAGER, Multiplexing::REQUIRE_WAIT], true)) {
+            throw new ConnectException('The stream handler cannot guarantee a multiplexed protocol; required multiplexing needs a cURL handler.', $request);
+        }
+
+        if ($this->connectionCapsConfigured && !empty($options['stream'])) {
+            throw new \InvalidArgumentException('Enabling the "stream" request option on a stream handler configured with the "max_host_connections" or "max_total_connections" option is not supported because streamed connections cannot be capped.');
+        }
+
+        if (isset($options['on_trailers'])) {
+            throw new \InvalidArgumentException('Passing the "on_trailers" request option to the stream handler is not supported because the stream handler cannot observe trailers.');
+        }
+
         $protocolVersion = $request->getProtocolVersion();
         $protocolVersion = $request->getProtocolVersion();
 
 
+        if ('' === $protocolVersion) {
+            \trigger_deprecation('guzzlehttp/guzzle', '7.11', 'Sending a request with an empty protocol version is deprecated; guzzlehttp/guzzle 8.0 will reject empty protocol versions.');
+
+            $protocolVersion = '1.1';
+            $request = Psr7\Utils::modifyRequest($request, ['version' => $protocolVersion]);
+        }
+
         if ('1.0' !== $protocolVersion && '1.1' !== $protocolVersion) {
         if ('1.0' !== $protocolVersion && '1.1' !== $protocolVersion) {
             throw new ConnectException(sprintf('HTTP/%s is not supported by the stream handler.', $protocolVersion), $request);
             throw new ConnectException(sprintf('HTTP/%s is not supported by the stream handler.', $protocolVersion), $request);
         }
         }
 
 
         $startTime = isset($options['on_stats']) ? Utils::currentTime() : null;
         $startTime = isset($options['on_stats']) ? Utils::currentTime() : null;
 
 
+        self::triggerUnsupportedRequestOptionDeprecations($request, $options);
+        $this->assertTransportSharingSupported();
+
         try {
         try {
             // Does not support the expect header.
             // Does not support the expect header.
             $request = $request->withoutHeader('Expect');
             $request = $request->withoutHeader('Expect');
@@ -56,8 +171,8 @@ class StreamHandler
             // the behavior of `CurlHandler`
             // the behavior of `CurlHandler`
             if (
             if (
                 (
                 (
-                    0 === \strcasecmp('PUT', $request->getMethod())
-                    || 0 === \strcasecmp('POST', $request->getMethod())
+                    Psr7\Utils::caselessEquals('PUT', $request->getMethod())
+                    || Psr7\Utils::caselessEquals('POST', $request->getMethod())
                 )
                 )
                 && 0 === $request->getBody()->getSize()
                 && 0 === $request->getBody()->getSize()
             ) {
             ) {
@@ -73,17 +188,10 @@ class StreamHandler
         } catch (\InvalidArgumentException $e) {
         } catch (\InvalidArgumentException $e) {
             throw $e;
             throw $e;
         } catch (\Exception $e) {
         } catch (\Exception $e) {
-            // Determine if the error was a networking error.
-            $message = $e->getMessage();
-            // This list can probably get more comprehensive.
-            if (false !== \strpos($message, 'getaddrinfo') // DNS lookup failed
-                || false !== \strpos($message, 'Connection refused')
-                || false !== \strpos($message, "couldn't connect to host") // error on HHVM
-                || false !== \strpos($message, 'connection attempt failed')
-            ) {
-                $e = new ConnectException($e->getMessage(), $request, $e);
-            } else {
-                $e = RequestException::wrapException($request, $e);
+            if (!$e instanceof TransferException) {
+                $e = self::isConnectionError($e->getMessage())
+                    ? new ConnectException($e->getMessage(), $request, $e)
+                    : new RequestException($e->getMessage(), $request, null, $e);
             }
             }
             $this->invokeStats($options, $request, $startTime, null, $e);
             $this->invokeStats($options, $request, $startTime, null, $e);
 
 
@@ -91,6 +199,17 @@ class StreamHandler
         }
         }
     }
     }
 
 
+    private static function isConnectionError(string $message): bool
+    {
+        foreach (self::CONNECTION_ERRORS as $connectionError) {
+            if (false !== \strpos($message, $connectionError)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
     private function invokeStats(
     private function invokeStats(
         array $options,
         array $options,
         RequestInterface $request,
         RequestInterface $request,
@@ -114,32 +233,28 @@ class StreamHandler
 
 
         try {
         try {
             [$ver, $status, $reason, $headers] = HeaderProcessor::parseHeaders($hdrs);
             [$ver, $status, $reason, $headers] = HeaderProcessor::parseHeaders($hdrs);
-        } catch (\Exception $e) {
-            return P\Create::rejectionFor(
-                new RequestException('An error was encountered while creating the response', $request, null, $e)
-            );
+        } catch (\Throwable $e) {
+            return $this->rejectResponseCreation($options, $request, $startTime, $e);
         }
         }
 
 
         [$stream, $headers] = $this->checkDecode($options, $headers, $stream);
         [$stream, $headers] = $this->checkDecode($options, $headers, $stream);
         $stream = Psr7\Utils::streamFor($stream);
         $stream = Psr7\Utils::streamFor($stream);
         $sink = $stream;
         $sink = $stream;
 
 
-        if (\strcasecmp('HEAD', $request->getMethod())) {
+        if (!Psr7\Utils::caselessEquals('HEAD', $request->getMethod())) {
             $sink = $this->createSink($stream, $options);
             $sink = $this->createSink($stream, $options);
         }
         }
 
 
         try {
         try {
             $response = new Psr7\Response($status, $headers, $sink, $ver, $reason);
             $response = new Psr7\Response($status, $headers, $sink, $ver, $reason);
-        } catch (\Exception $e) {
-            return P\Create::rejectionFor(
-                new RequestException('An error was encountered while creating the response', $request, null, $e)
-            );
+        } catch (\Throwable $e) {
+            return $this->rejectResponseCreation($options, $request, $startTime, $e);
         }
         }
 
 
         if (isset($options['on_headers'])) {
         if (isset($options['on_headers'])) {
             try {
             try {
                 $options['on_headers']($response);
                 $options['on_headers']($response);
-            } catch (\Exception $e) {
+            } catch (\Throwable $e) {
                 return P\Create::rejectionFor(
                 return P\Create::rejectionFor(
                     new RequestException('An error was encountered during the on_headers event', $request, $response, $e)
                     new RequestException('An error was encountered during the on_headers event', $request, $response, $e)
                 );
                 );
@@ -157,6 +272,24 @@ class StreamHandler
         return new FulfilledPromise($response);
         return new FulfilledPromise($response);
     }
     }
 
 
+    private function rejectResponseCreation(
+        array $options,
+        RequestInterface $request,
+        ?float $startTime,
+        \Throwable $previous
+    ): PromiseInterface {
+        $reason = new RequestException(
+            'An error was encountered while creating the response',
+            $request,
+            null,
+            $previous
+        );
+
+        $this->invokeStats($options, $request, $startTime, null, $reason);
+
+        return P\Create::rejectionFor($reason);
+    }
+
     private function createSink(StreamInterface $stream, array $options): StreamInterface
     private function createSink(StreamInterface $stream, array $options): StreamInterface
     {
     {
         if (!empty($options['stream'])) {
         if (!empty($options['stream'])) {
@@ -174,7 +307,7 @@ class StreamHandler
     private function checkDecode(array $options, array $headers, $stream): array
     private function checkDecode(array $options, array $headers, $stream): array
     {
     {
         // Automatically decode responses when instructed.
         // Automatically decode responses when instructed.
-        if (!empty($options['decode_content'])) {
+        if (isset($options['decode_content']) && $options['decode_content'] !== false) {
             $normalizedKeys = Utils::normalizeHeaderKeys($headers);
             $normalizedKeys = Utils::normalizeHeaderKeys($headers);
             if (isset($normalizedKeys['content-encoding'])) {
             if (isset($normalizedKeys['content-encoding'])) {
                 $encoding = $headers[$normalizedKeys['content-encoding']];
                 $encoding = $headers[$normalizedKeys['content-encoding']];
@@ -185,15 +318,12 @@ class StreamHandler
                     // Remove content-encoding header
                     // Remove content-encoding header
                     unset($headers[$normalizedKeys['content-encoding']]);
                     unset($headers[$normalizedKeys['content-encoding']]);
 
 
-                    // Fix content-length header
+                    // The decoded length cannot be known without inflating the
+                    // stream, so keep the original length for inspection and
+                    // drop the now-unknown Content-Length header.
                     if (isset($normalizedKeys['content-length'])) {
                     if (isset($normalizedKeys['content-length'])) {
                         $headers['x-encoded-content-length'] = $headers[$normalizedKeys['content-length']];
                         $headers['x-encoded-content-length'] = $headers[$normalizedKeys['content-length']];
-                        $length = (int) $stream->getSize();
-                        if ($length === 0) {
-                            unset($headers[$normalizedKeys['content-length']]);
-                        } else {
-                            $headers[$normalizedKeys['content-length']] = [$length];
-                        }
+                        unset($headers[$normalizedKeys['content-length']]);
                     }
                     }
                 }
                 }
             }
             }
@@ -263,7 +393,7 @@ class StreamHandler
                     $message .= "[$key] $value".\PHP_EOL;
                     $message .= "[$key] $value".\PHP_EOL;
                 }
                 }
             }
             }
-            throw new \RuntimeException(\trim($message));
+            throw new \RuntimeException(\trim($message, " \n\r\t\0\x0B"));
         }
         }
 
 
         return $resource;
         return $resource;
@@ -279,10 +409,27 @@ class StreamHandler
             $methods = \array_flip(\get_class_methods(__CLASS__));
             $methods = \array_flip(\get_class_methods(__CLASS__));
         }
         }
 
 
-        if (!\in_array($request->getUri()->getScheme(), ['http', 'https'])) {
-            throw new RequestException(\sprintf("The scheme '%s' is not supported.", $request->getUri()->getScheme()), $request);
+        $uri = $request->getUri();
+        $scheme = $uri->getScheme();
+        if ($scheme === '') {
+            throw new RequestException('URI must include a scheme and host. Use an absolute URI, a network-path reference starting with //, or configure a base_uri.', $request);
+        }
+
+        if (!\in_array($scheme, ['http', 'https'], true)) {
+            throw new RequestException(\sprintf("The scheme '%s' is not supported.", $scheme), $request);
+        }
+
+        $protocols = Utils::normalizeProtocols($options['protocols'] ?? ['http', 'https']);
+        if (!\in_array($scheme, $protocols, true)) {
+            throw new RequestException(\sprintf('The scheme "%s" is not allowed by the protocols request option.', $scheme), $request);
+        }
+
+        if ($uri->getHost() === '') {
+            throw new RequestException('URI must include a scheme and host. Use an absolute URI, a network-path reference starting with //, or configure a base_uri.', $request);
         }
         }
 
 
+        HostValidator::assertRequestHost($request);
+
         // HTTP/1.1 streams using the PHP stream wrapper require a
         // HTTP/1.1 streams using the PHP stream wrapper require a
         // Connection: close header
         // Connection: close header
         if ($request->getProtocolVersion() === '1.1'
         if ($request->getProtocolVersion() === '1.1'
@@ -303,10 +450,19 @@ class StreamHandler
             throw new \InvalidArgumentException('on_headers must be callable');
             throw new \InvalidArgumentException('on_headers must be callable');
         }
         }
 
 
+        self::assertTlsVersionRangeForOptions($options);
+
+        $proxyAuthorizationAdded = false;
         if (!empty($options)) {
         if (!empty($options)) {
             foreach ($options as $key => $value) {
             foreach ($options as $key => $value) {
                 $method = "add_{$key}";
                 $method = "add_{$key}";
                 if (isset($methods[$method])) {
                 if (isset($methods[$method])) {
+                    if ($method === 'add_proxy') {
+                        $proxyAuthorizationAdded = $this->add_proxy($request, $context, $value, $params);
+
+                        continue;
+                    }
+
                     $this->{$method}($request, $context, $value, $params);
                     $this->{$method}($request, $context, $value, $params);
                 }
                 }
             }
             }
@@ -316,6 +472,16 @@ class StreamHandler
             if (!\is_array($options['stream_context'])) {
             if (!\is_array($options['stream_context'])) {
                 throw new \InvalidArgumentException('stream_context must be an array');
                 throw new \InvalidArgumentException('stream_context must be an array');
             }
             }
+            if (
+                $proxyAuthorizationAdded
+                && isset($options['stream_context']['http'])
+                && \is_array($options['stream_context']['http'])
+                && \array_key_exists('proxy', $options['stream_context']['http'])
+            ) {
+                throw new \InvalidArgumentException('stream_context.http.proxy cannot override a proxy after the stream handler has generated a Proxy-Authorization header; configure the final proxy with the "proxy" request option.');
+            }
+            self::triggerConflictingStreamContextOptionDeprecations($options['stream_context']);
+            self::triggerUnsupportedStreamContextOptionDeprecations($options['stream_context']);
             $context = \array_replace_recursive($context, $options['stream_context']);
             $context = \array_replace_recursive($context, $options['stream_context']);
         }
         }
 
 
@@ -333,12 +499,18 @@ class StreamHandler
         );
         );
 
 
         return $this->createResource(
         return $this->createResource(
-            function () use ($uri, &$http_response_header, $contextResource, $context, $options, $request) {
+            function () use ($uri, $contextResource, $context, $options, $request) {
                 $resource = @\fopen((string) $uri, 'r', false, $contextResource);
                 $resource = @\fopen((string) $uri, 'r', false, $contextResource);
+
+                // See https://wiki.php.net/rfc/deprecations_php_8_5#deprecate_the_http_response_header_predefined_variable
+                if (function_exists('http_get_last_response_headers')) {
+                    $http_response_header = \http_get_last_response_headers();
+                }
+
                 $this->lastHeaders = $http_response_header ?? [];
                 $this->lastHeaders = $http_response_header ?? [];
 
 
                 if (false === $resource) {
                 if (false === $resource) {
-                    throw new ConnectException(sprintf('Connection refused for URI %s', $uri), $request, null, $context);
+                    throw new ConnectException(sprintf('Connection refused for URI %s', Psr7\Utils::redactUserInfo($uri)), $request, null, $context);
                 }
                 }
 
 
                 if (isset($options['read_timeout'])) {
                 if (isset($options['read_timeout'])) {
@@ -357,7 +529,23 @@ class StreamHandler
     {
     {
         $uri = $request->getUri();
         $uri = $request->getUri();
 
 
-        if (isset($options['force_ip_resolve']) && !\filter_var($uri->getHost(), \FILTER_VALIDATE_IP)) {
+        $host = $uri->getHost();
+
+        // Fold a numeric IPv4 spelling to the dotted quad libcurl connects
+        // to, rather than leaving it to the platform resolver: macOS reads
+        // the zero-padded 0177 as decimal 177 where glibc, musl and FreeBSD
+        // read octal 127. The Host header is serialized from the request and
+        // stays as written; the TLS peer name follows the same fold.
+        $canonicalHost = self::canonicalConnectionHost($host);
+        if ($canonicalHost !== $host) {
+            $uri = $uri->withHost($canonicalHost);
+            $host = $canonicalHost;
+        }
+
+        $hostForIpCheck = $host !== '' && $host[0] === '[' && \substr($host, -1) === ']'
+            ? \substr($host, 1, -1)
+            : $host;
+        if (isset($options['force_ip_resolve']) && !\filter_var($hostForIpCheck, \FILTER_VALIDATE_IP)) {
             if ('v4' === $options['force_ip_resolve']) {
             if ('v4' === $options['force_ip_resolve']) {
                 $records = \dns_get_record($uri->getHost(), \DNS_A);
                 $records = \dns_get_record($uri->getHost(), \DNS_A);
                 if (false === $records || !isset($records[0]['ip'])) {
                 if (false === $records || !isset($records[0]['ip'])) {
@@ -379,10 +567,99 @@ class StreamHandler
         return $uri;
         return $uri;
     }
     }
 
 
+    /**
+     * Returns a numeric IPv4 spelling folded to the dotted quad libcurl's
+     * ipv4_normalize() produces, and every other host unchanged.
+     */
+    private static function canonicalConnectionHost(string $host): string
+    {
+        $binary = self::numericIpv4ToBinary($host);
+        if ($binary === null) {
+            return $host;
+        }
+
+        return (string) \inet_ntop($binary);
+    }
+
+    /**
+     * Returns the four-byte binary form of a host that a transport reads as a
+     * numeric IPv4 address, or null when it reads it as a name.
+     *
+     * The shape test is HostValidator::isNumericIpv4Host(); this method adds
+     * the range checks that predicate omits: every part but the last must fit
+     * one octet, and the last must fit the octets the earlier parts left. A
+     * trailing root dot is not swallowed, unlike libcurl 8.21.0 and later,
+     * because assertRequestHost() rejects that spelling first.
+     */
+    private static function numericIpv4ToBinary(string $host): ?string
+    {
+        if (!HostValidator::isNumericIpv4Host($host)) {
+            return null;
+        }
+
+        $values = [];
+        foreach (\explode('.', $host) as $part) {
+            $values[] = self::numericIpv4PartValue($part);
+        }
+
+        // Every accepted value is a whole number no larger than 0xFFFFFFFF,
+        // which a float holds exactly, so the arithmetic below is correct on a
+        // 32-bit build too, where the widest part overflows an integer.
+        $address = (float) \array_pop($values);
+
+        $packed = '';
+        foreach ($values as $value) {
+            if ($value > 255.0) {
+                return null;
+            }
+
+            $packed .= \chr((int) $value);
+        }
+
+        $width = 4 - \count($values);
+        if ($address >= 256.0 ** $width) {
+            return null;
+        }
+
+        for ($shift = $width - 1; $shift >= 0; --$shift) {
+            $packed .= \chr((int) \fmod(\floor($address / 256.0 ** $shift), 256.0));
+        }
+
+        return $packed;
+    }
+
+    /**
+     * Returns the value of one accepted part as a float, so a part filling
+     * all four octets such as 2130706433 stays exact on every integer width.
+     */
+    private static function numericIpv4PartValue(string $part): float
+    {
+        if ($part[0] === '0' && isset($part[1]) && ($part[1] === 'x' || $part[1] === 'X')) {
+            return (float) \hexdec((string) \substr($part, 2));
+        }
+
+        if ($part[0] === '0') {
+            return (float) \octdec($part);
+        }
+
+        return (float) $part;
+    }
+
     private function getDefaultContext(RequestInterface $request): array
     private function getDefaultContext(RequestInterface $request): array
     {
     {
         $headers = '';
         $headers = '';
         foreach ($request->getHeaders() as $name => $value) {
         foreach ($request->getHeaders() as $name => $value) {
+            // A first-class Proxy-Authorization header is proxy-scoped. Keep
+            // it out of the origin context; add_proxy() adds one
+            // validated canonical line only when Guzzle selects a proxy; PHP
+            // extracts that line for CONNECT and removes it before sending the
+            // tunneled origin request. The caselessEquals() helper is
+            // locale-independent, unlike strcasecmp(), so a locale cannot
+            // make this match miss and re-leak the credential.
+            if (Psr7\Utils::caselessEquals((string) $name, 'Proxy-Authorization')) {
+                continue;
+            }
+
             foreach ($value as $val) {
             foreach ($value as $val) {
                 $headers .= "$name: $val\r\n";
                 $headers .= "$name: $val\r\n";
             }
             }
@@ -397,7 +674,7 @@ class StreamHandler
                 'follow_location' => 0,
                 'follow_location' => 0,
             ],
             ],
             'ssl' => [
             'ssl' => [
-                'peer_name' => $request->getUri()->getHost(),
+                'peer_name' => self::canonicalConnectionHost($request->getUri()->getHost()),
             ],
             ],
         ];
         ];
 
 
@@ -411,15 +688,271 @@ class StreamHandler
             }
             }
         }
         }
 
 
-        $context['http']['header'] = \rtrim($context['http']['header']);
+        $context['http']['header'] = \rtrim($context['http']['header'], " \n\r\t\0\x0B");
 
 
         return $context;
         return $context;
     }
     }
 
 
+    private static function triggerUnsupportedRequestOptionDeprecations(RequestInterface $request, array $options): void
+    {
+        if (
+            \array_key_exists('curl', $options)
+            && $options['curl'] !== null
+            && $options['curl'] !== []
+            && !self::isCurlOptionGeneratedByAuth($options)
+        ) {
+            \trigger_deprecation('guzzlehttp/guzzle', '7.11', 'Passing the "curl" request option to the stream handler is deprecated; guzzlehttp/guzzle 8.0 will reject this option because the stream handler ignores cURL options.');
+        }
+
+        if (\array_key_exists('expect', $options) && $options['expect'] !== false && $request->hasHeader('Expect')) {
+            \trigger_deprecation('guzzlehttp/guzzle', '7.11', 'Passing the "expect" request option to the stream handler is deprecated when it adds an Expect header; guzzlehttp/guzzle 8.0 will reject this option because the stream handler does not support Expect: 100-Continue.');
+        }
+    }
+
+    private static function triggerConflictingStreamContextOptionDeprecations(array $streamContext): void
+    {
+        $conflictingOptions = self::conflictingStreamContextOptions();
+
+        foreach ($streamContext as $wrapper => $contextOptions) {
+            if (!\is_string($wrapper) || !isset($conflictingOptions[$wrapper]) || !\is_array($contextOptions)) {
+                continue;
+            }
+
+            foreach ($contextOptions as $option => $_) {
+                if (!\is_string($option) || !\array_key_exists($option, $conflictingOptions[$wrapper])) {
+                    continue;
+                }
+
+                \trigger_deprecation(
+                    'guzzlehttp/guzzle',
+                    '7.12',
+                    \sprintf(
+                        'Passing stream_context.%s.%s in the "stream_context" request option is deprecated; guzzlehttp/guzzle 8.0 will reject this option because it conflicts with Guzzle-managed request handling. Use %s instead.',
+                        $wrapper,
+                        $option,
+                        $conflictingOptions[$wrapper][$option]
+                    )
+                );
+            }
+        }
+    }
+
+    private static function triggerUnsupportedStreamContextOptionDeprecations(array $streamContext): void
+    {
+        $unsupportedOptions = self::unsupportedStreamContextOptions($streamContext);
+        if ($unsupportedOptions === []) {
+            return;
+        }
+
+        \trigger_deprecation(
+            'guzzlehttp/guzzle',
+            '7.12',
+            \sprintf(
+                'Passing PHP stream context options outside the built-in stream handler allow-list to the "stream_context" request option is deprecated; guzzlehttp/guzzle 8.0 will reject stream context options outside the allow-list. Deprecated option%s: %s.',
+                \count($unsupportedOptions) === 1 ? '' : 's',
+                \implode(', ', $unsupportedOptions)
+            )
+        );
+    }
+
+    /**
+     * @return string[]
+     */
+    private static function unsupportedStreamContextOptions(array $streamContext): array
+    {
+        $supportedOptions = self::supportedStreamContextOptions();
+        $conflictingOptions = self::conflictingStreamContextOptions();
+        $unsupportedOptions = [];
+
+        foreach ($streamContext as $wrapper => $contextOptions) {
+            if (!\is_string($wrapper) || !isset($supportedOptions[$wrapper])) {
+                if (\is_array($contextOptions)) {
+                    foreach ($contextOptions as $option => $_) {
+                        if (\is_string($wrapper) && \is_string($option) && isset($conflictingOptions[$wrapper]) && \array_key_exists($option, $conflictingOptions[$wrapper])) {
+                            continue;
+                        }
+
+                        $unsupportedOptions[] = \sprintf('stream_context.%s.%s', (string) $wrapper, (string) $option);
+                    }
+                } else {
+                    $unsupportedOptions[] = \sprintf('stream_context.%s', (string) $wrapper);
+                }
+
+                continue;
+            }
+
+            if (!\is_array($contextOptions)) {
+                $unsupportedOptions[] = \sprintf('stream_context.%s', $wrapper);
+
+                continue;
+            }
+
+            foreach ($contextOptions as $option => $_) {
+                if (\is_string($option) && isset($conflictingOptions[$wrapper]) && \array_key_exists($option, $conflictingOptions[$wrapper])) {
+                    continue;
+                }
+
+                if (!\is_string($option) || !\array_key_exists($option, $supportedOptions[$wrapper])) {
+                    $unsupportedOptions[] = \sprintf('stream_context.%s.%s', $wrapper, (string) $option);
+                }
+            }
+        }
+
+        return $unsupportedOptions;
+    }
+
+    /**
+     * @return array<string, array<string, true>>
+     */
+    private static function supportedStreamContextOptions(): array
+    {
+        return [
+            'http' => [
+                'request_fulluri' => true,
+            ],
+            'socket' => [
+                'bindto' => true,
+                'tcp_nodelay' => true,
+            ],
+            'ssl' => [
+                'SNI_enabled' => true,
+                'capture_peer_cert' => true,
+                'capture_peer_cert_chain' => true,
+                'ciphers' => true,
+                'disable_compression' => true,
+                'no_ticket' => true,
+                'peer_fingerprint' => true,
+                'security_level' => true,
+                'verify_depth' => true,
+            ],
+        ];
+    }
+
+    /**
+     * @return array<string, array<string, string>>
+     */
+    private static function conflictingStreamContextOptions(): array
+    {
+        return [
+            'http' => [
+                'content' => 'the request body',
+                'follow_location' => 'the "allow_redirects" request option',
+                'header' => 'the request headers',
+                'max_redirects' => 'the "allow_redirects" request option',
+                'method' => 'the request method',
+                'protocol_version' => 'the request protocol version',
+                'proxy' => 'the "proxy" request option',
+                'timeout' => 'the "timeout" request option',
+            ],
+            'ssl' => [
+                'allow_self_signed' => 'the "verify" request option',
+                'cafile' => 'the "verify" request option',
+                'capath' => 'the "verify" request option',
+                'crypto_method' => 'the "crypto_method" request option',
+                'local_cert' => 'the "cert" request option',
+                'local_pk' => 'the "ssl_key" request option',
+                'max_proto_version' => 'the "crypto_method_max" request option',
+                'min_proto_version' => 'the "crypto_method" request option',
+                'passphrase' => 'the "cert" or "ssl_key" request option',
+                'peer_name' => 'the request URI',
+                'verify_peer' => 'the "verify" request option',
+                'verify_peer_name' => 'the "verify" request option',
+            ],
+        ];
+    }
+
+    private function assertTransportSharingSupported(): void
+    {
+        if ($this->transportSharingMode === TransportSharing::HANDLER_REQUIRE) {
+            throw new \InvalidArgumentException('The "transport_sharing" option requires transport sharing, but the stream handler does not support it.');
+        }
+    }
+
+    private static function isCurlOptionGeneratedByAuth(array $options): bool
+    {
+        if (!isset($options['curl']) || !\is_array($options['curl']) || !isset($options['auth'][2]) || !\is_string($options['auth'][2])) {
+            return false;
+        }
+
+        if (!\defined('CURLOPT_HTTPAUTH') || !\defined('CURLOPT_USERPWD')) {
+            return false;
+        }
+
+        $type = Psr7\Utils::asciiToLower($options['auth'][2]);
+        if ($type === 'digest') {
+            $httpAuth = \defined('CURLAUTH_DIGEST') ? \constant('CURLAUTH_DIGEST') : null;
+        } elseif ($type === 'ntlm') {
+            $httpAuth = \defined('CURLAUTH_NTLM') ? \constant('CURLAUTH_NTLM') : null;
+        } else {
+            return false;
+        }
+
+        return $httpAuth !== null
+            && \count($options['curl']) === 2
+            && isset($options['curl'][\CURLOPT_HTTPAUTH], $options['curl'][\CURLOPT_USERPWD])
+            && $options['curl'][\CURLOPT_HTTPAUTH] === $httpAuth;
+    }
+
     /**
     /**
      * @param mixed $value as passed via Request transfer options.
      * @param mixed $value as passed via Request transfer options.
+     *
+     * @return array{0: string, 1: string|null}
      */
      */
-    private function add_proxy(RequestInterface $request, array &$options, $value, array &$params): void
+    private static function normalizeTlsFileOption(string $option, $value): array
+    {
+        $passphrase = null;
+
+        if (\is_array($value)) {
+            if (!isset($value[0]) || !\is_string($value[0])) {
+                throw new \InvalidArgumentException(\sprintf('Invalid %s request option', $option));
+            }
+            if (isset($value[1])) {
+                if (!\is_string($value[1])) {
+                    throw new \InvalidArgumentException(\sprintf('Invalid %s request option', $option));
+                }
+                $passphrase = $value[1];
+            }
+            $value = $value[0];
+        }
+
+        if (!\is_string($value)) {
+            throw new \InvalidArgumentException(\sprintf('Invalid %s request option', $option));
+        }
+
+        return [$value, $passphrase];
+    }
+
+    private static function setTlsPassphrase(array &$options, ?string $passphrase, string $option): void
+    {
+        if ($passphrase === null) {
+            return;
+        }
+
+        if (isset($options['ssl']['passphrase']) && $options['ssl']['passphrase'] !== $passphrase) {
+            throw new \InvalidArgumentException(\sprintf('Cannot use different passphrases for cert and ssl_key with the stream handler; %s conflicts with an existing TLS passphrase.', $option));
+        }
+
+        $options['ssl']['passphrase'] = $passphrase;
+    }
+
+    /**
+     * @param mixed $value as passed via Request transfer options.
+     */
+    private static function assertStreamTlsType(string $option, $value): void
+    {
+        if (!\is_string($value) || $value === '') {
+            throw new \InvalidArgumentException(\sprintf('%s must be a non-empty string', $option));
+        }
+
+        if (Psr7\Utils::asciiToUpper($value) !== 'PEM') {
+            throw new \InvalidArgumentException(\sprintf('The stream handler only supports "PEM" for the %s request option.', $option));
+        }
+    }
+
+    /**
+     * @param mixed $value as passed via Request transfer options.
+     */
+    private function add_proxy(RequestInterface $request, array &$options, $value, array &$params): bool
     {
     {
         $uri = null;
         $uri = null;
 
 
@@ -428,25 +961,50 @@ class StreamHandler
         } else {
         } else {
             $scheme = $request->getUri()->getScheme();
             $scheme = $request->getUri()->getScheme();
             if (isset($value[$scheme])) {
             if (isset($value[$scheme])) {
-                if (!isset($value['no']) || !Utils::isHostInNoProxy($request->getUri()->getHost(), $value['no'])) {
+                if (
+                    !isset($value['no'])
+                    || !Utils::isUriInNoProxy($request->getUri(), $value['no'])
+                ) {
                     $uri = $value[$scheme];
                     $uri = $value[$scheme];
                 }
                 }
             }
             }
         }
         }
 
 
         if (!$uri) {
         if (!$uri) {
-            return;
+            return false;
         }
         }
 
 
         $parsed = $this->parse_proxy($uri);
         $parsed = $this->parse_proxy($uri);
+
+        // PHP extracts and removes only one Proxy-Authorization line for a
+        // CONNECT tunnel. Serialize exactly one validated first-class value;
+        // more than one could leave a credential in the tunneled origin
+        // request. A first-class value, including an empty one, is
+        // authoritative over Basic credentials embedded in the proxy URI.
+        $managed = $request->getHeader('Proxy-Authorization');
+        if (\count($managed) > 1) {
+            throw new \InvalidArgumentException('The stream handler supports exactly one Proxy-Authorization request header value when a proxy is selected.');
+        }
+        if ($managed !== [] && \strpbrk($managed[0], "\r\n") !== false) {
+            throw new \InvalidArgumentException('Proxy-Authorization request header values must not contain a carriage return or line feed.');
+        }
+
         $options['http']['proxy'] = $parsed['proxy'];
         $options['http']['proxy'] = $parsed['proxy'];
 
 
-        if ($parsed['auth']) {
-            if (!isset($options['http']['header'])) {
-                $options['http']['header'] = [];
-            }
+        if (($managed !== [] || $parsed['auth']) && !isset($options['http']['header'])) {
+            $options['http']['header'] = '';
+        }
+        if ($managed !== []) {
+            $options['http']['header'] .= "\r\nProxy-Authorization: {$managed[0]}";
+
+            return true;
+        } elseif ($parsed['auth']) {
             $options['http']['header'] .= "\r\nProxy-Authorization: {$parsed['auth']}";
             $options['http']['header'] .= "\r\nProxy-Authorization: {$parsed['auth']}";
+
+            return true;
         }
         }
+
+        return false;
     }
     }
 
 
     /**
     /**
@@ -456,16 +1014,29 @@ class StreamHandler
     {
     {
         $parsed = \parse_url($url);
         $parsed = \parse_url($url);
 
 
-        if ($parsed !== false && isset($parsed['scheme']) && $parsed['scheme'] === 'http') {
-            if (isset($parsed['host']) && isset($parsed['port'])) {
-                $auth = null;
-                if (isset($parsed['user']) && isset($parsed['pass'])) {
-                    $auth = \base64_encode("{$parsed['user']}:{$parsed['pass']}");
-                }
+        // parse_url() misreads scheme-less proxy authorities like
+        // "user:pass@host"; re-parse only those forms as HTTP.
+        $schemeLessAuthority = \strpos($url, '://') === false && \strncmp($url, '//', 2) !== 0;
+        if ($schemeLessAuthority) {
+            if (\is_array($parsed) && !isset($parsed['scheme']) && isset($parsed['host'], $parsed['port'])) {
+                $parsed['scheme'] = 'http';
+            } elseif (
+                (!\is_array($parsed) || !isset($parsed['host']))
+                && (\strpos($url, '@') !== false || \strncmp($url, '[', 1) === 0)
+            ) {
+                $parsed = \parse_url('http://'.$url);
+            }
+        }
+
+        if (\is_array($parsed) && isset($parsed['scheme']) && Psr7\Utils::caselessEquals($parsed['scheme'], 'http')) {
+            if (isset($parsed['host'], $parsed['port'])) {
+                $user = $parsed['user'] ?? '';
+                $pass = $parsed['pass'] ?? '';
+                $auth = ($user !== '' || $pass !== '') ? 'Basic '.\base64_encode("{$user}:{$pass}") : null;
 
 
                 return [
                 return [
                     'proxy' => "tcp://{$parsed['host']}:{$parsed['port']}",
                     'proxy' => "tcp://{$parsed['host']}:{$parsed['port']}",
-                    'auth' => $auth ? "Basic {$auth}" : null,
+                    'auth' => $auth,
                 ];
                 ];
             }
             }
         }
         }
@@ -506,6 +1077,26 @@ class StreamHandler
         throw new \InvalidArgumentException('Invalid crypto_method request option: unknown version provided');
         throw new \InvalidArgumentException('Invalid crypto_method request option: unknown version provided');
     }
     }
 
 
+    /**
+     * @param mixed $value as passed via Request transfer options.
+     */
+    private function add_crypto_method_max(RequestInterface $request, array &$options, $value, array &$params): void
+    {
+        $options['ssl']['max_proto_version'] = TlsVersion::streamProtocolVersion('crypto_method_max', $value);
+    }
+
+    private static function assertTlsVersionRangeForOptions(array $options): void
+    {
+        if (!isset($options['crypto_method_max'])) {
+            return;
+        }
+
+        TlsVersion::assertRange(
+            $options['crypto_method'] ?? null,
+            $options['crypto_method_max']
+        );
+    }
+
     /**
     /**
      * @param mixed $value as passed via Request transfer options.
      * @param mixed $value as passed via Request transfer options.
      */
      */
@@ -537,23 +1128,56 @@ class StreamHandler
      */
      */
     private function add_cert(RequestInterface $request, array &$options, $value, array &$params): void
     private function add_cert(RequestInterface $request, array &$options, $value, array &$params): void
     {
     {
-        if (\is_array($value)) {
-            $options['ssl']['passphrase'] = $value[1];
-            $value = $value[0];
-        }
+        [$value, $passphrase] = self::normalizeTlsFileOption('cert', $value);
 
 
         if (!\file_exists($value)) {
         if (!\file_exists($value)) {
             throw new \RuntimeException("SSL certificate not found: {$value}");
             throw new \RuntimeException("SSL certificate not found: {$value}");
         }
         }
 
 
+        self::setTlsPassphrase($options, $passphrase, 'cert');
         $options['ssl']['local_cert'] = $value;
         $options['ssl']['local_cert'] = $value;
     }
     }
 
 
+    /**
+     * @param mixed $value as passed via Request transfer options.
+     */
+    private function add_cert_type(RequestInterface $request, array &$options, $value, array &$params): void
+    {
+        self::assertStreamTlsType('cert_type', $value);
+    }
+
+    /**
+     * @param mixed $value as passed via Request transfer options.
+     */
+    private function add_ssl_key(RequestInterface $request, array &$options, $value, array &$params): void
+    {
+        [$value, $passphrase] = self::normalizeTlsFileOption('ssl_key', $value);
+
+        if (!\file_exists($value)) {
+            throw new \RuntimeException("SSL private key not found: {$value}");
+        }
+
+        self::setTlsPassphrase($options, $passphrase, 'ssl_key');
+        $options['ssl']['local_pk'] = $value;
+    }
+
+    /**
+     * @param mixed $value as passed via Request transfer options.
+     */
+    private function add_ssl_key_type(RequestInterface $request, array &$options, $value, array &$params): void
+    {
+        self::assertStreamTlsType('ssl_key_type', $value);
+    }
+
     /**
     /**
      * @param mixed $value as passed via Request transfer options.
      * @param mixed $value as passed via Request transfer options.
      */
      */
     private function add_progress(RequestInterface $request, array &$options, $value, array &$params): void
     private function add_progress(RequestInterface $request, array &$options, $value, array &$params): void
     {
     {
+        if (!\is_callable($value)) {
+            throw new \InvalidArgumentException('progress client option must be callable');
+        }
+
         self::addNotification(
         self::addNotification(
             $params,
             $params,
             static function ($code, $a, $b, $c, $transferred, $total) use ($value) {
             static function ($code, $a, $b, $c, $transferred, $total) use ($value) {

+ 84 - 0
modules/boonex/xero/plugins/guzzlehttp/guzzle/src/Handler/TlsVersion.php

@@ -0,0 +1,84 @@
+<?php
+
+namespace GuzzleHttp\Handler;
+
+/**
+ * @internal
+ */
+final class TlsVersion
+{
+    /**
+     * @param mixed $value
+     */
+    public static function ordinal(string $option, $value): int
+    {
+        if ($value === \STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT) {
+            return 10;
+        }
+        if ($value === \STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT) {
+            return 11;
+        }
+        if ($value === \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT) {
+            return 12;
+        }
+        if (\defined('STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT') && $value === \STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT) {
+            return 13;
+        }
+
+        throw new \InvalidArgumentException(\sprintf('Invalid %s request option: unknown version provided', $option));
+    }
+
+    /**
+     * @param mixed $min
+     * @param mixed $max
+     */
+    public static function assertRange($min, $max): void
+    {
+        if ($min === null || $max === null) {
+            return;
+        }
+
+        if (self::ordinal('crypto_method_max', $max) < self::ordinal('crypto_method', $min)) {
+            throw new \InvalidArgumentException('Invalid crypto_method_max request option: maximum TLS version must be greater than or equal to crypto_method');
+        }
+    }
+
+    /**
+     * @param mixed $value
+     */
+    public static function streamProtocolVersion(string $option, $value): int
+    {
+        if ($value === \STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT) {
+            return self::requireStreamProto('STREAM_CRYPTO_PROTO_TLSv1_0', $option);
+        }
+        if ($value === \STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT) {
+            return self::requireStreamProto('STREAM_CRYPTO_PROTO_TLSv1_1', $option);
+        }
+        if ($value === \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT) {
+            return self::requireStreamProto('STREAM_CRYPTO_PROTO_TLSv1_2', $option);
+        }
+        if (\defined('STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT') && $value === \STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT) {
+            return self::requireStreamProto('STREAM_CRYPTO_PROTO_TLSv1_3', $option);
+        }
+
+        throw new \InvalidArgumentException(\sprintf('Invalid %s request option: unknown version provided', $option));
+    }
+
+    /**
+     * Resolves a STREAM_CRYPTO_PROTO_* constant. The ssl.max_proto_version
+     * context option and these constants were added in PHP 7.3.0 (TLS 1.3 in
+     * 7.4.0); on older runtimes the option cannot be honored, so reject loudly.
+     */
+    private static function requireStreamProto(string $constant, string $option): int
+    {
+        if (\defined($constant)) {
+            /** @var int */
+            return \constant($constant);
+        }
+
+        throw new \InvalidArgumentException(\sprintf(
+            'Invalid %s request option: maximum TLS version control is not supported by your version of PHP',
+            $option
+        ));
+    }
+}

+ 19 - 5
modules/boonex/xero/plugins/guzzlehttp/guzzle/src/HandlerStack.php

@@ -181,15 +181,29 @@ class HandlerStack
     public function remove($remove): void
     public function remove($remove): void
     {
     {
         if (!is_string($remove) && !is_callable($remove)) {
         if (!is_string($remove) && !is_callable($remove)) {
-            trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a callable or string to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
+            \trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a callable or string to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
         }
         }
 
 
         $this->cached = null;
         $this->cached = null;
-        $idx = \is_callable($remove) ? 0 : 1;
+
+        if (\is_string($remove)) {
+            $count = \count($this->stack);
+            $this->stack = \array_values(\array_filter(
+                $this->stack,
+                static function ($tuple) use ($remove) {
+                    return $tuple[1] !== $remove;
+                }
+            ));
+
+            if ($count !== \count($this->stack) || !\is_callable($remove)) {
+                return;
+            }
+        }
+
         $this->stack = \array_values(\array_filter(
         $this->stack = \array_values(\array_filter(
             $this->stack,
             $this->stack,
-            static function ($tuple) use ($idx, $remove) {
-                return $tuple[$idx] !== $remove;
+            static function ($tuple) use ($remove) {
+                return $tuple[0] !== $remove;
             }
             }
         ));
         ));
     }
     }
@@ -270,6 +284,6 @@ class HandlerStack
         }
         }
 
 
         /** @var object $fn */
         /** @var object $fn */
-        return 'callable('.\spl_object_hash($fn).')';
+        return 'callable('.\spl_object_id($fn).')';
     }
     }
 }
 }

+ 9 - 4
modules/boonex/xero/plugins/guzzlehttp/guzzle/src/MessageFormatter.php

@@ -72,8 +72,7 @@ class MessageFormatter implements MessageFormatterInterface
     {
     {
         $cache = [];
         $cache = [];
 
 
-        /** @var string */
-        return \preg_replace_callback(
+        $result = \preg_replace_callback(
             '/{\s*([A-Za-z_\-\.0-9]+)\s*}/',
             '/{\s*([A-Za-z_\-\.0-9]+)\s*}/',
             function (array $matches) use ($request, $response, $error, &$cache) {
             function (array $matches) use ($request, $response, $error, &$cache) {
                 if (isset($cache[$matches[1]])) {
                 if (isset($cache[$matches[1]])) {
@@ -90,7 +89,7 @@ class MessageFormatter implements MessageFormatterInterface
                         break;
                         break;
                     case 'req_headers':
                     case 'req_headers':
                         $result = \trim($request->getMethod()
                         $result = \trim($request->getMethod()
-                                .' '.$request->getRequestTarget())
+                                .' '.$request->getRequestTarget(), " \n\r\t\0\x0B")
                             .' HTTP/'.$request->getProtocolVersion()."\r\n"
                             .' HTTP/'.$request->getProtocolVersion()."\r\n"
                             .$this->headers($request);
                             .$this->headers($request);
                         break;
                         break;
@@ -182,6 +181,12 @@ class MessageFormatter implements MessageFormatterInterface
             },
             },
             $this->template
             $this->template
         );
         );
+
+        if ($result === null) {
+            throw new \RuntimeException('Unable to format message: '.\preg_last_error_msg());
+        }
+
+        return $result;
     }
     }
 
 
     /**
     /**
@@ -194,6 +199,6 @@ class MessageFormatter implements MessageFormatterInterface
             $result .= $name.': '.\implode(', ', $values)."\r\n";
             $result .= $name.': '.\implode(', ', $values)."\r\n";
         }
         }
 
 
-        return \trim($result);
+        return \trim($result, " \n\r\t\0\x0B");
     }
     }
 }
 }

+ 3 - 3
modules/boonex/xero/plugins/guzzlehttp/guzzle/src/Middleware.php

@@ -29,7 +29,7 @@ final class Middleware
             return static function ($request, array $options) use ($handler) {
             return static function ($request, array $options) use ($handler) {
                 if (empty($options['cookies'])) {
                 if (empty($options['cookies'])) {
                     return $handler($request, $options);
                     return $handler($request, $options);
-                } elseif (!($options['cookies'] instanceof CookieJarInterface)) {
+                } elseif (!$options['cookies'] instanceof CookieJarInterface) {
                     throw new \InvalidArgumentException('cookies must be an instance of GuzzleHttp\Cookie\CookieJarInterface');
                     throw new \InvalidArgumentException('cookies must be an instance of GuzzleHttp\Cookie\CookieJarInterface');
                 }
                 }
                 $cookieJar = $options['cookies'];
                 $cookieJar = $options['cookies'];
@@ -187,12 +187,12 @@ final class Middleware
      * Middleware that logs requests, responses, and errors using a message
      * Middleware that logs requests, responses, and errors using a message
      * formatter.
      * formatter.
      *
      *
-     * @phpstan-param \Psr\Log\LogLevel::* $logLevel  Level at which to log requests.
-     *
      * @param LoggerInterface                            $logger    Logs messages.
      * @param LoggerInterface                            $logger    Logs messages.
      * @param MessageFormatterInterface|MessageFormatter $formatter Formatter used to create message strings.
      * @param MessageFormatterInterface|MessageFormatter $formatter Formatter used to create message strings.
      * @param string                                     $logLevel  Level at which to log requests.
      * @param string                                     $logLevel  Level at which to log requests.
      *
      *
+     * @phpstan-param \Psr\Log\LogLevel::* $logLevel Level at which to log requests.
+     *
      * @return callable Returns a function that accepts the next handler.
      * @return callable Returns a function that accepts the next handler.
      */
      */
     public static function log(LoggerInterface $logger, $formatter, string $logLevel = 'info'): callable
     public static function log(LoggerInterface $logger, $formatter, string $logLevel = 'info'): callable

+ 26 - 0
modules/boonex/xero/plugins/guzzlehttp/guzzle/src/Multiplexing.php

@@ -0,0 +1,26 @@
+<?php
+
+namespace GuzzleHttp;
+
+/**
+ * Multiplexing modes for the "multiplex" request option.
+ *
+ * NONE disables multiplexing for a whole handler when passed as the
+ * "multiplex" client configuration option or, when constructing a handler
+ * directly, as the CurlMultiHandler "multiplex" constructor option. As a
+ * request option value it guarantees the transfer does not share its
+ * connection with any concurrent transfer, and is accepted only where that
+ * guarantee holds.
+ */
+final class Multiplexing
+{
+    public const NONE = 'none';
+    public const EAGER = 'eager';
+    public const WAIT = 'wait';
+    public const REQUIRE_EAGER = 'require_eager';
+    public const REQUIRE_WAIT = 'require_wait';
+
+    private function __construct()
+    {
+    }
+}

+ 13 - 1
modules/boonex/xero/plugins/guzzlehttp/guzzle/src/Pool.php

@@ -51,6 +51,18 @@ class Pool implements PromisorInterface
             $opts = [];
             $opts = [];
         }
         }
 
 
+        if (!\is_iterable($requests)) {
+            \trigger_deprecation(
+                'guzzlehttp/guzzle',
+                '7.11',
+                'Passing a non-iterable request collection to %s::__construct() or %s::batch() is deprecated; guzzlehttp/guzzle 8.0 will require an iterable.',
+                __CLASS__,
+                __CLASS__
+            );
+
+            $requests = [$requests];
+        }
+
         $iterable = P\Create::iterFor($requests);
         $iterable = P\Create::iterFor($requests);
         $requests = static function () use ($iterable, $client, $opts) {
         $requests = static function () use ($iterable, $client, $opts) {
             foreach ($iterable as $key => $rfn) {
             foreach ($iterable as $key => $rfn) {
@@ -59,7 +71,7 @@ class Pool implements PromisorInterface
                 } elseif (\is_callable($rfn)) {
                 } elseif (\is_callable($rfn)) {
                     yield $key => $rfn($opts);
                     yield $key => $rfn($opts);
                 } else {
                 } else {
-                    throw new \InvalidArgumentException('Each value yielded by the iterator must be a Psr7\Http\Message\RequestInterface or a callable that returns a promise that fulfills with a Psr7\Message\Http\ResponseInterface object.');
+                    throw new \InvalidArgumentException('Each value yielded by the iterator must be a Psr\Http\Message\RequestInterface or a callable that returns a promise that fulfills with a Psr\Http\Message\ResponseInterface object.');
                 }
                 }
             }
             }
         };
         };

+ 1 - 1
modules/boonex/xero/plugins/guzzlehttp/guzzle/src/PrepareBodyMiddleware.php

@@ -52,7 +52,7 @@ class PrepareBodyMiddleware
         ) {
         ) {
             $size = $request->getBody()->getSize();
             $size = $request->getBody()->getSize();
             if ($size !== null) {
             if ($size !== null) {
-                $modify['set_headers']['Content-Length'] = $size;
+                $modify['set_headers']['Content-Length'] = (string) $size;
             } else {
             } else {
                 $modify['set_headers']['Transfer-Encoding'] = 'chunked';
                 $modify['set_headers']['Transfer-Encoding'] = 'chunked';
             }
             }

+ 28 - 8
modules/boonex/xero/plugins/guzzlehttp/guzzle/src/RedirectMiddleware.php

@@ -3,6 +3,7 @@
 namespace GuzzleHttp;
 namespace GuzzleHttp;
 
 
 use GuzzleHttp\Exception\BadResponseException;
 use GuzzleHttp\Exception\BadResponseException;
+use GuzzleHttp\Exception\RequestException;
 use GuzzleHttp\Exception\TooManyRedirectsException;
 use GuzzleHttp\Exception\TooManyRedirectsException;
 use GuzzleHttp\Promise\PromiseInterface;
 use GuzzleHttp\Promise\PromiseInterface;
 use Psr\Http\Message\RequestInterface;
 use Psr\Http\Message\RequestInterface;
@@ -13,7 +14,7 @@ use Psr\Http\Message\UriInterface;
  * Request redirect middleware.
  * Request redirect middleware.
  *
  *
  * Apply this middleware like other middleware using
  * Apply this middleware like other middleware using
- * {@see \GuzzleHttp\Middleware::redirect()}.
+ * {@see Middleware::redirect()}.
  *
  *
  * @final
  * @final
  */
  */
@@ -104,6 +105,10 @@ class RedirectMiddleware
             );
             );
         }
         }
 
 
+        // The caller's delay applies once, before the initial request, not
+        // before each followed redirect.
+        unset($options['delay']);
+
         $promise = $this($nextRequest, $options);
         $promise = $this($nextRequest, $options);
 
 
         // Add headers to be able to track history of redirects.
         // Add headers to be able to track history of redirects.
@@ -169,28 +174,43 @@ class RedirectMiddleware
         if ($statusCode == 303
         if ($statusCode == 303
             || ($statusCode <= 302 && !$options['allow_redirects']['strict'])
             || ($statusCode <= 302 && !$options['allow_redirects']['strict'])
         ) {
         ) {
-            $safeMethods = ['GET', 'HEAD', 'OPTIONS'];
             $requestMethod = $request->getMethod();
             $requestMethod = $request->getMethod();
 
 
-            $modify['method'] = in_array($requestMethod, $safeMethods) ? $requestMethod : 'GET';
-            $modify['body'] = '';
+            if ($requestMethod !== 'QUERY' || !\in_array($statusCode, [301, 302], true)) {
+                $modify['method'] = \in_array($requestMethod, ['GET', 'HEAD', 'OPTIONS'], true) ? $requestMethod : 'GET';
+                $modify['body'] = '';
+                $modify['remove_headers'] = ['Content-Length', 'Transfer-Encoding'];
+            }
         }
         }
 
 
         $uri = self::redirectUri($request, $response, $protocols);
         $uri = self::redirectUri($request, $response, $protocols);
-        if (isset($options['idn_conversion']) && ($options['idn_conversion'] !== false)) {
-            $idnOptions = ($options['idn_conversion'] === true) ? \IDNA_DEFAULT : $options['idn_conversion'];
+        $idnOptions = Utils::normalizeIdnConversionOption($options['idn_conversion'] ?? null);
+        if ($idnOptions !== null) {
             $uri = Utils::idnUriConvert($uri, $idnOptions);
             $uri = Utils::idnUriConvert($uri, $idnOptions);
         }
         }
 
 
         $modify['uri'] = $uri;
         $modify['uri'] = $uri;
-        Psr7\Message::rewindBody($request);
+
+        // The body only needs to be rewound when the next request reuses it.
+        if (!isset($modify['body'])) {
+            try {
+                Psr7\Message::rewindBody($request);
+            } catch (\RuntimeException $e) {
+                throw new RequestException(
+                    'Redirect failed because the request body could not be rewound: '.$e->getMessage(),
+                    $request,
+                    $response,
+                    $e
+                );
+            }
+        }
 
 
         // Add the Referer header if it is told to do so and only
         // Add the Referer header if it is told to do so and only
         // add the header if we are not redirecting from https to http.
         // add the header if we are not redirecting from https to http.
         if ($options['allow_redirects']['referer']
         if ($options['allow_redirects']['referer']
             && $modify['uri']->getScheme() === $request->getUri()->getScheme()
             && $modify['uri']->getScheme() === $request->getUri()->getScheme()
         ) {
         ) {
-            $uri = $request->getUri()->withUserInfo('');
+            $uri = $request->getUri()->withUserInfo('')->withFragment('');
             $modify['set_headers']['Referer'] = (string) $uri;
             $modify['set_headers']['Referer'] = (string) $uri;
         } else {
         } else {
             $modify['remove_headers'][] = 'Referer';
             $modify['remove_headers'][] = 'Referer';

+ 217 - 57
modules/boonex/xero/plugins/guzzlehttp/guzzle/src/RequestOptions.php

@@ -5,7 +5,7 @@ namespace GuzzleHttp;
 /**
 /**
  * This class contains a list of built-in Guzzle request options.
  * This class contains a list of built-in Guzzle request options.
  *
  *
- * @see https://docs.guzzlephp.org/en/latest/request-options.html
+ * @see https://github.com/guzzle/guzzle/blob/7.15/docs/request-options.md
  */
  */
 final class RequestOptions
 final class RequestOptions
 {
 {
@@ -20,44 +20,58 @@ final class RequestOptions
      * - max: (int, default=5) maximum number of allowed redirects.
      * - max: (int, default=5) maximum number of allowed redirects.
      * - strict: (bool, default=false) Set to true to use strict redirects
      * - strict: (bool, default=false) Set to true to use strict redirects
      *   meaning redirect POST requests with POST requests vs. doing what most
      *   meaning redirect POST requests with POST requests vs. doing what most
-     *   browsers do which is redirect POST requests with GET requests
+     *   browsers do which is redirect POST requests with GET requests. The
+     *   QUERY method keeps its method and body across non-strict 301 and 302
+     *   redirects, and a 303 redirect is followed with a body-less GET.
      * - referer: (bool, default=false) Set to true to enable the Referer
      * - referer: (bool, default=false) Set to true to enable the Referer
      *   header.
      *   header.
-     * - protocols: (array, default=['http', 'https']) Allowed redirect
-     *   protocols.
+     * - protocols: (non-empty-array<array-key, string>, default=['http', 'https'])
+     *   Allowed redirect protocols. Redirect matching is case-sensitive; use
+     *   "http" and "https".
      * - on_redirect: (callable) PHP callable that is invoked when a redirect
      * - on_redirect: (callable) PHP callable that is invoked when a redirect
      *   is encountered. The callable is invoked with the request, the redirect
      *   is encountered. The callable is invoked with the request, the redirect
      *   response that was received, and the effective URI. Any return value
      *   response that was received, and the effective URI. Any return value
      *   from the on_redirect function is ignored.
      *   from the on_redirect function is ignored.
+     * - track_redirects: (bool, default=false) Track redirected URI and status
+     *   history in response headers.
      */
      */
     public const ALLOW_REDIRECTS = 'allow_redirects';
     public const ALLOW_REDIRECTS = 'allow_redirects';
 
 
     /**
     /**
-     * auth: (array) Pass an array of HTTP authentication parameters to use
-     * with the request. The array must contain the username in index [0],
-     * the password in index [1], and you can optionally provide a built-in
-     * authentication type in index [2]. Pass null to disable authentication
-     * for a request.
+     * auth: (array{0: string, 1: string, 2?: string|null}|string|false|null)
+     * Pass an array of HTTP authentication parameters to use with the request.
+     * The array must contain the username in index [0], the password in index
+     * [1], and you can optionally provide a built-in authentication type in
+     * index [2]. Pass false or null to disable authentication for a request.
+     * String values are passed through for custom handlers.
      */
      */
     public const AUTH = 'auth';
     public const AUTH = 'auth';
 
 
     /**
     /**
-     * body: (resource|string|null|int|float|StreamInterface|callable|\Iterator)
-     * Body to send in the request.
+     * body: (resource|string|null|int|float|bool|\Psr\Http\Message\StreamInterface|(callable&object)|\Iterator|\Stringable)
+     * Body to send in the request. Callable arrays are arrays, and arrays are
+     * not valid body values in Guzzle.
      */
      */
     public const BODY = 'body';
     public const BODY = 'body';
 
 
     /**
     /**
-     * cert: (string|array) Set to a string to specify the path to a file
-     * containing a PEM formatted SSL client side certificate. If a password
-     * is required, then set cert to an array containing the path to the PEM
-     * file in the first array element followed by the certificate password
-     * in the second array element.
+     * cert: (string|array{0: string, 1?: string|null}) Set to a string to
+     * specify the path to a client certificate file. PEM is the default
+     * certificate format. If a password is required, set cert to an array
+     * containing the certificate path in the first array element followed by
+     * the certificate password in the second array element. A null password is
+     * treated the same as omitting it. Use cert_type to specify another
+     * supported certificate format.
      */
      */
     public const CERT = 'cert';
     public const CERT = 'cert';
 
 
     /**
     /**
-     * cookies: (bool|GuzzleHttp\Cookie\CookieJarInterface, default=false)
+     * cert_type: (string) Specify the SSL client certificate file type.
+     */
+    public const CERT_TYPE = 'cert_type';
+
+    /**
+     * cookies: (false|GuzzleHttp\Cookie\CookieJarInterface, default=false)
      * Specifies whether or not cookies are used in a request or what cookie
      * Specifies whether or not cookies are used in a request or what cookie
      * jar to use or what cookies to send. This option only works if your
      * jar to use or what cookies to send. This option only works if your
      * handler has the `cookie` middleware. Valid values are `false` and
      * handler has the `cookie` middleware. Valid values are `false` and
@@ -66,9 +80,9 @@ final class RequestOptions
     public const COOKIES = 'cookies';
     public const COOKIES = 'cookies';
 
 
     /**
     /**
-     * connect_timeout: (float, default=0) Float describing the number of
-     * seconds to wait while trying to connect to a server. Use 0 to wait
-     * 300 seconds (the default behavior).
+     * connect_timeout: (int|float, default=0) Number of seconds to wait while
+     * trying to connect to a server. Use 0 to wait 300 seconds (the default
+     * behavior).
      */
      */
     public const CONNECT_TIMEOUT = 'connect_timeout';
     public const CONNECT_TIMEOUT = 'connect_timeout';
 
 
@@ -84,6 +98,23 @@ final class RequestOptions
      */
      */
     public const CRYPTO_METHOD = 'crypto_method';
     public const CRYPTO_METHOD = 'crypto_method';
 
 
+    /**
+     * crypto_method_max: (int) A value describing the maximum TLS protocol
+     * version to use.
+     *
+     * This setting must be set to one of the
+     * ``STREAM_CRYPTO_METHOD_TLS*_CLIENT`` constants. On the stream handler,
+     * PHP 7.3 or higher is required to set a maximum TLS version, and PHP 7.4
+     * or higher is required to use TLS 1.3. cURL 7.54.0 or higher is required
+     * in order to specify a maximum TLS version with the cURL handler.
+     */
+    public const CRYPTO_METHOD_MAX = 'crypto_method_max';
+
+    /**
+     * curl: (array) Raw cURL options to apply when using a built-in cURL handler.
+     */
+    public const CURL = 'curl';
+
     /**
     /**
      * debug: (bool|resource) Set to true or set to a PHP stream returned by
      * debug: (bool|resource) Set to true or set to a PHP stream returned by
      * fopen()  enable debug output with the HTTP handler used to send a
      * fopen()  enable debug output with the HTTP handler used to send a
@@ -92,14 +123,15 @@ final class RequestOptions
     public const DEBUG = 'debug';
     public const DEBUG = 'debug';
 
 
     /**
     /**
-     * decode_content: (bool, default=true) Specify whether or not
+     * decode_content: (bool|string, default=true) Specify whether or not
      * Content-Encoding responses (gzip, deflate, etc.) are automatically
      * Content-Encoding responses (gzip, deflate, etc.) are automatically
      * decoded.
      * decoded.
      */
      */
     public const DECODE_CONTENT = 'decode_content';
     public const DECODE_CONTENT = 'decode_content';
 
 
     /**
     /**
-     * delay: (int) The amount of time to delay before sending in milliseconds.
+     * delay: (int|float) The amount of time to delay before sending in
+     * milliseconds.
      */
      */
     public const DELAY = 'delay';
     public const DELAY = 'delay';
 
 
@@ -122,16 +154,17 @@ final class RequestOptions
     public const EXPECT = 'expect';
     public const EXPECT = 'expect';
 
 
     /**
     /**
-     * form_params: (array) Associative array of form field names to values
-     * where each value is a string or array of strings. Sets the Content-Type
-     * header to application/x-www-form-urlencoded when no Content-Type header
-     * is already present.
+     * form_params: (array<array-key, string|int|float|bool|null|array>)
+     * Associative array of form field names to scalar, null, or nested array
+     * values. Sets the Content-Type header to application/x-www-form-urlencoded
+     * when no Content-Type header is already present.
      */
      */
     public const FORM_PARAMS = 'form_params';
     public const FORM_PARAMS = 'form_params';
 
 
     /**
     /**
-     * headers: (array) Associative array of HTTP headers. Each value MUST be
-     * a string or array of strings.
+     * headers: (array<array-key, string|non-empty-array<array-key, string>>|null)
+     * Associative array of HTTP headers. Each value MUST be a string or non-empty
+     * array of strings.
      */
      */
     public const HEADERS = 'headers';
     public const HEADERS = 'headers';
 
 
@@ -144,10 +177,10 @@ final class RequestOptions
     public const HTTP_ERRORS = 'http_errors';
     public const HTTP_ERRORS = 'http_errors';
 
 
     /**
     /**
-     * idn: (bool|int, default=true) A combination of IDNA_* constants for
-     * idn_to_ascii() PHP's function (see "options" parameter). Set to false to
-     * disable IDN support completely, or to true to use the default
-     * configuration (IDNA_DEFAULT constant).
+     * idn_conversion: (bool|int|null, default=false) A combination of IDNA_*
+     * constants for PHP's idn_to_ascii() function. Set to false or null to
+     * disable IDN support, or to true to use the default configuration
+     * (IDNA_DEFAULT constant).
      */
      */
     public const IDN_CONVERSION = 'idn_conversion';
     public const IDN_CONVERSION = 'idn_conversion';
 
 
@@ -159,16 +192,103 @@ final class RequestOptions
     public const JSON = 'json';
     public const JSON = 'json';
 
 
     /**
     /**
-     * multipart: (array) Array of associative arrays, each containing a
-     * required "name" key mapping to the form field, name, a required
-     * "contents" key mapping to a StreamInterface|resource|string, an
-     * optional "headers" associative array of custom headers, and an
-     * optional "filename" key mapping to a string to send as the filename in
-     * the part. If no "filename" key is present, then no "filename" attribute
-     * will be added to the part.
+     * multipart: (array) Array of part arrays, each containing a required
+     * "name" key mapping to the string or integer form field name, a required
+     * "contents" key mapping to any non-array value accepted by PSR-7
+     * Utils::streamFor() or a nested array of field values, an optional
+     * "headers" array of string custom header values, and an optional
+     * "filename" key mapping to a string to send as the filename in the part.
+     * "headers" and "filename" cannot be used when "contents" is an array.
      */
      */
     public const MULTIPART = 'multipart';
     public const MULTIPART = 'multipart';
 
 
+    /**
+     * multiplex: (string) Controls how a request sent through a built-in
+     * cURL handler relates to shared, multiplexed connections: how an HTTP/2
+     * request pursues one, or, with Multiplexing::NONE, whether the transfer
+     * may share its connection at all. When the option is not set,
+     * multiplexing is left to libcurl: nothing waits, and established
+     * multiplex-capable connections are still shared. Use
+     * Multiplexing::EAGER to explicitly never wait for pending connections,
+     * Multiplexing::WAIT to wait on libcurl-eligible pending connections with
+     * CURLOPT_PIPEWAIT, normally to the same origin,
+     * Multiplexing::REQUIRE_EAGER to fail unless a multiplexed protocol is
+     * guaranteed while dialing eagerly, or Multiplexing::REQUIRE_WAIT for the
+     * same guarantee while also waiting on pending connections. The required
+     * modes require a handler that permits actual multiplexing, not merely a
+     * multiplexed protocol, and are rejected on a Multiplexing::NONE handler.
+     * The stream handler ignores EAGER and WAIT, and rejects the required
+     * family; CurlHandler has no multi handle to multiplex over. Explicit
+     * modes reject deprecated raw cURL options they conflict with: the
+     * required family cannot be combined with a raw CURLOPT_HTTP_VERSION,
+     * CURLOPT_URL, or CURLOPT_FOLLOWLOCATION; no explicit mode can be
+     * combined with a raw CURLOPT_PIPEWAIT on the CurlMultiHandler; and
+     * Multiplexing::NONE on a CurlMultiHandler that permits multiplexing
+     * cannot be combined with the raw CURLOPT_HTTP_VERSION, CURLOPT_HTTPAUTH
+     * (including the "auth" request option's "digest" and "ntlm" modes,
+     * which set it), CURLOPT_PROXYAUTH, CURLOPT_FOLLOWLOCATION,
+     * CURLOPT_HTTPHEADER, CURLOPT_ALTSVC, CURLOPT_ALTSVC_CTRL, or
+     * CURLOPT_PROXYTYPE cURL options. The required family also
+     * rejects final CURLOPT_HTTPAUTH masks that permit NTLM, which libcurl
+     * retries over HTTP/1.1. The required family validates its cleartext
+     * proxy rule against the final cURL configuration, after raw options
+     * such as CURLOPT_PROXY and CURLOPT_PRE_PROXY are applied; only the
+     * exact raw CURLOPT_NOPROXY wildcard '*' disables the primary proxy and
+     * pre-proxy there, and raw host-specific patterns are conservatively
+     * treated as leaving them active. These rejections are
+     * configuration-conflict checks, not remote security checks.
+     *
+     * Multiplexing::NONE disables multiplexing for a whole handler when
+     * passed as the "multiplex" client configuration option, which
+     * configures the default handler and also becomes the default request
+     * option, or, when constructing a handler directly, as the
+     * CurlMultiHandler "multiplex" constructor option. A handler
+     * configured with Multiplexing::NONE rejects explicitly requested wait
+     * modes as a configuration conflict when the transfer would actually
+     * wait, and always rejects the required modes, because they require a
+     * handler that permits actual multiplexing, not merely a multiplexed
+     * protocol. As a request option value, Multiplexing::NONE guarantees the
+     * transfer does not share its connection with any concurrent transfer.
+     * Multiplexing::NONE does not force HTTP/1.1: on a Multiplexing::NONE
+     * handler, HTTP/2 still negotiates and each transfer keeps its
+     * connection to itself.
+     *
+     * The request option value is accepted exactly where the guarantee
+     * holds and can be verified: on a CurlMultiHandler configured with
+     * Multiplexing::NONE, for requests whose declared protocol version is
+     * HTTP/1.x, on CurlHandler, and on the stream handler, which never
+     * multiplexes. An HTTP/2 request with a Multiplexing::NONE request
+     * option is rejected on a CurlMultiHandler that permits multiplexing.
+     * On a CurlMultiHandler that permits multiplexing, Multiplexing::NONE
+     * is also rejected with a custom "handle_factory", alongside a raw
+     * CURLMOPT_PIPELINING cURL multi option, and combined with the raw
+     * CURLOPT_HTTP_VERSION, CURLOPT_HTTPAUTH (including the "auth" request
+     * option's "digest" and "ntlm" modes, which set it), CURLOPT_PROXYAUTH,
+     * CURLOPT_FOLLOWLOCATION, CURLOPT_HTTPHEADER, CURLOPT_ALTSVC,
+     * CURLOPT_ALTSVC_CTRL, or CURLOPT_PROXYTYPE cURL options. It is also
+     * rejected when the request carries an Expect: 100-continue header (its
+     * 417 retries select connections outside the safeguards; remove an
+     * explicitly supplied header, or set the "expect" request option to
+     * false to prevent it being added automatically).
+     *
+     * On a client whose multi handler permits multiplexing, the ordinary
+     * non-streaming default stack - both cURL handlers available and no
+     * connection caps forcing multi-only routing - runs synchronous
+     * requests on the CurlHandler path, which satisfies the guarantee for
+     * any protocol version, while asynchronous requests run on the
+     * CurlMultiHandler, so an HTTP/2 request with Multiplexing::NONE
+     * succeeds synchronously and is rejected asynchronously on the same
+     * client. Keep-alive reuse between consecutive transfers is
+     * unaffected, except on libcurl versions below 7.77.0 and from 8.11.0
+     * through 8.12.1, where an accepted HTTP/1.x request on a multiplexing
+     * CurlMultiHandler forces a fresh connection. Custom handlers receive
+     * the "multiplex" option unchanged: its semantics are handler-defined,
+     * Guzzle does not guarantee it is honored, and a client-level
+     * Multiplexing::NONE with a custom handler flows to it as a default
+     * request option without client-side enforcement.
+     */
+    public const MULTIPLEX = 'multiplex';
+
     /**
     /**
      * on_headers: (callable) A callable that is invoked when the HTTP headers
      * on_headers: (callable) A callable that is invoked when the HTTP headers
      * of the response have been received but the body has not yet begun to
      * of the response have been received but the body has not yet begun to
@@ -187,6 +307,17 @@ final class RequestOptions
      */
      */
     public const ON_STATS = 'on_stats';
     public const ON_STATS = 'on_stats';
 
 
+    /**
+     * on_trailers: (callable) A callable that is invoked by the built-in cURL
+     * handlers once per successful transfer, after the response body has been
+     * received, with an associative array of the parsed HTTP trailers followed
+     * by the response. Trailer field names are lowercased and grouped
+     * case-insensitively; values keep their wire order. Malformed trailer
+     * field lines are discarded before parsing. Trailer fields are reported
+     * separately from response headers and are never merged into the response.
+     */
+    public const ON_TRAILERS = 'on_trailers';
+
     /**
     /**
      * progress: (callable) Defines a function to invoke when transfer
      * progress: (callable) Defines a function to invoke when transfer
      * progress is made. The function accepts the following positional
      * progress is made. The function accepts the following positional
@@ -196,25 +327,34 @@ final class RequestOptions
      */
      */
     public const PROGRESS = 'progress';
     public const PROGRESS = 'progress';
 
 
+    /**
+     * protocols: (non-empty-array<array-key, string>, default=['http', 'https'])
+     * Allowed URI schemes. Built-in handlers accept only the case-sensitive
+     * values "http" and "https".
+     */
+    public const PROTOCOLS = 'protocols';
+
     /**
     /**
      * proxy: (string|array) Pass a string to specify an HTTP proxy, or an
      * proxy: (string|array) Pass a string to specify an HTTP proxy, or an
      * array to specify different proxies for different protocols (where the
      * array to specify different proxies for different protocols (where the
-     * key is the protocol and the value is a proxy string).
+     * key is the protocol and the value is a proxy string or null). Provide a
+     * "no" key as a comma-delimited string, array of strings, or null to
+     * specify hosts or host-and-port pairs that should not be proxied.
      */
      */
     public const PROXY = 'proxy';
     public const PROXY = 'proxy';
 
 
     /**
     /**
-     * query: (array|string) Associative array of query string values to add
-     * to the request. This option uses PHP's http_build_query() to create
-     * the string representation. Pass a string value if you need more
-     * control than what this method provides
+     * query: (array<array-key, mixed>|string) Associative array of query string
+     * values to add to the request. This option uses PHP's http_build_query()
+     * to create the string representation. Pass a string value if you need
+     * more control than what this method provides
      */
      */
     public const QUERY = 'query';
     public const QUERY = 'query';
 
 
     /**
     /**
-     * sink: (resource|string|StreamInterface) Where the data of the
-     * response is written to. Defaults to a PHP temp stream. Providing a
-     * string will write data to a file by the given name.
+     * sink: (resource|string|\Psr\Http\Message\StreamInterface) Where the data
+     * of the response is written to. Defaults to a PHP temp stream. Providing
+     * a string will write data to a file by the given name.
      */
      */
     public const SINK = 'sink';
     public const SINK = 'sink';
 
 
@@ -227,19 +367,32 @@ final class RequestOptions
     public const SYNCHRONOUS = 'synchronous';
     public const SYNCHRONOUS = 'synchronous';
 
 
     /**
     /**
-     * ssl_key: (array|string) Specify the path to a file containing a private
-     * SSL key in PEM format. If a password is required, then set to an array
-     * containing the path to the SSL key in the first array element followed
-     * by the password required for the certificate in the second element.
+     * ssl_key: (array{0: string, 1?: string|null}|string) Specify the path to
+     * a private SSL key file. PEM is the default private key format. If a
+     * password is required, set ssl_key to an array containing the key path in
+     * the first array element followed by the key password in the second
+     * element. A null password is treated the same as omitting it. Use
+     * ssl_key_type to specify another supported key format.
      */
      */
     public const SSL_KEY = 'ssl_key';
     public const SSL_KEY = 'ssl_key';
 
 
     /**
     /**
-     * stream: Set to true to attempt to stream a response rather than
+     * ssl_key_type: (string) Specify the SSL private key file type.
+     */
+    public const SSL_KEY_TYPE = 'ssl_key_type';
+
+    /**
+     * stream: (bool) Set to true to attempt to stream a response rather than
      * download it all up-front.
      * download it all up-front.
      */
      */
     public const STREAM = 'stream';
     public const STREAM = 'stream';
 
 
+    /**
+     * stream_context: (array) PHP stream context options to merge into the
+     * context used by the built-in stream handler.
+     */
+    public const STREAM_CONTEXT = 'stream_context';
+
     /**
     /**
      * verify: (bool|string, default=true) Describes the SSL certificate
      * verify: (bool|string, default=true) Describes the SSL certificate
      * verification behavior of a request. Set to true to enable SSL
      * verification behavior of a request. Set to true to enable SSL
@@ -251,24 +404,31 @@ final class RequestOptions
     public const VERIFY = 'verify';
     public const VERIFY = 'verify';
 
 
     /**
     /**
-     * timeout: (float, default=0) Float describing the timeout of the
+     * timeout: (int|float, default=0) Number describing the timeout of the
      * request in seconds. Use 0 to wait indefinitely (the default behavior).
      * request in seconds. Use 0 to wait indefinitely (the default behavior).
      */
      */
     public const TIMEOUT = 'timeout';
     public const TIMEOUT = 'timeout';
 
 
     /**
     /**
-     * read_timeout: (float, default=default_socket_timeout ini setting) Float describing
-     * the body read timeout, for stream requests.
+     * read_timeout: (int|float, default=default_socket_timeout ini setting)
+     * Number describing the body read timeout, for stream requests.
      */
      */
     public const READ_TIMEOUT = 'read_timeout';
     public const READ_TIMEOUT = 'read_timeout';
 
 
     /**
     /**
-     * version: (float) Specifies the HTTP protocol version to attempt to use.
+     * retries: (int) Current retry count used by the retry middleware.
+     */
+    public const RETRIES = 'retries';
+
+    /**
+     * version: (string|int|float) Specifies the HTTP protocol version to attempt
+     * to use.
      */
      */
     public const VERSION = 'version';
     public const VERSION = 'version';
 
 
     /**
     /**
-     * force_ip_resolve: (bool) Force client to use only ipv4 or ipv6 protocol
+     * force_ip_resolve: (string) Set to "v4" to force IPv4 resolution or "v6"
+     * for IPv6 resolution when supported by the handler.
      */
      */
     public const FORCE_IP_RESOLVE = 'force_ip_resolve';
     public const FORCE_IP_RESOLVE = 'force_ip_resolve';
 }
 }

+ 7 - 1
modules/boonex/xero/plugins/guzzlehttp/guzzle/src/RetryMiddleware.php

@@ -44,16 +44,22 @@ class RetryMiddleware
     {
     {
         $this->decider = $decider;
         $this->decider = $decider;
         $this->nextHandler = $nextHandler;
         $this->nextHandler = $nextHandler;
-        $this->delay = $delay ?: __CLASS__.'::exponentialDelay';
+        $this->delay = $delay ?: static function (int $retries): int {
+            return (int) 2 ** ($retries - 1) * 1000;
+        };
     }
     }
 
 
     /**
     /**
      * Default exponential backoff delay function.
      * Default exponential backoff delay function.
      *
      *
      * @return int milliseconds.
      * @return int milliseconds.
+     *
+     * @deprecated since 7.11, will be removed in 8.0.
      */
      */
     public static function exponentialDelay(int $retries): int
     public static function exponentialDelay(int $retries): int
     {
     {
+        \trigger_deprecation('guzzlehttp/guzzle', '7.11', '%s::%s() is deprecated and will be removed in 8.0.', __CLASS__, __FUNCTION__);
+
         return (int) 2 ** ($retries - 1) * 1000;
         return (int) 2 ** ($retries - 1) * 1000;
     }
     }
 
 

+ 14 - 0
modules/boonex/xero/plugins/guzzlehttp/guzzle/src/TransportSharing.php

@@ -0,0 +1,14 @@
+<?php
+
+namespace GuzzleHttp;
+
+final class TransportSharing
+{
+    public const NONE = 'none';
+    public const HANDLER_PREFER = 'handler_prefer';
+    public const HANDLER_REQUIRE = 'handler_require';
+
+    private function __construct()
+    {
+    }
+}

+ 606 - 48
modules/boonex/xero/plugins/guzzlehttp/guzzle/src/Utils.php

@@ -5,8 +5,11 @@ namespace GuzzleHttp;
 use GuzzleHttp\Exception\InvalidArgumentException;
 use GuzzleHttp\Exception\InvalidArgumentException;
 use GuzzleHttp\Handler\CurlHandler;
 use GuzzleHttp\Handler\CurlHandler;
 use GuzzleHttp\Handler\CurlMultiHandler;
 use GuzzleHttp\Handler\CurlMultiHandler;
+use GuzzleHttp\Handler\CurlShareHandleState;
+use GuzzleHttp\Handler\CurlVersion;
 use GuzzleHttp\Handler\Proxy;
 use GuzzleHttp\Handler\Proxy;
 use GuzzleHttp\Handler\StreamHandler;
 use GuzzleHttp\Handler\StreamHandler;
+use Psr\Http\Message\RequestInterface;
 use Psr\Http\Message\UriInterface;
 use Psr\Http\Message\UriInterface;
 
 
 final class Utils
 final class Utils
@@ -18,9 +21,18 @@ final class Utils
      *
      *
      * @return string Returns a string containing the type of the variable and
      * @return string Returns a string containing the type of the variable and
      *                if a class is provided, the class name.
      *                if a class is provided, the class name.
+     *
+     * @deprecated Utils::describeType() will be removed in guzzlehttp/guzzle:8.0. Use get_debug_type() instead.
      */
      */
     public static function describeType($input): string
     public static function describeType($input): string
     {
     {
+        \trigger_deprecation(
+            'guzzlehttp/guzzle',
+            '7.12',
+            '%s() is deprecated and will be removed in 8.0. Use get_debug_type() instead.',
+            __METHOD__
+        );
+
         switch (\gettype($input)) {
         switch (\gettype($input)) {
             case 'object':
             case 'object':
                 return 'object('.\get_class($input).')';
                 return 'object('.\get_class($input).')';
@@ -33,7 +45,7 @@ final class Utils
                 /** @var string $varDumpContent */
                 /** @var string $varDumpContent */
                 $varDumpContent = \ob_get_clean();
                 $varDumpContent = \ob_get_clean();
 
 
-                return \str_replace('double(', 'float(', \rtrim($varDumpContent));
+                return \str_replace('double(', 'float(', \rtrim($varDumpContent, " \n\r\t\0\x0B"));
         }
         }
     }
     }
 
 
@@ -49,7 +61,7 @@ final class Utils
 
 
         foreach ($lines as $line) {
         foreach ($lines as $line) {
             $parts = \explode(':', $line, 2);
             $parts = \explode(':', $line, 2);
-            $headers[\trim($parts[0])][] = isset($parts[1]) ? \trim($parts[1]) : null;
+            $headers[\trim($parts[0], " \n\r\t\0\x0B")][] = isset($parts[1]) ? \trim($parts[1], " \n\r\t\0\x0B") : null;
         }
         }
 
 
         return $headers;
         return $headers;
@@ -79,33 +91,156 @@ final class Utils
      *
      *
      * The returned handler is not wrapped by any default middlewares.
      * The returned handler is not wrapped by any default middlewares.
      *
      *
-     * @return callable(\Psr\Http\Message\RequestInterface, array): Promise\PromiseInterface Returns the best handler for the given system.
+     * @param array{transport_sharing?: mixed, max_host_connections?: mixed, max_total_connections?: mixed, multiplex?: mixed} $handlerOptions Handler constructor options.
+     *
+     * @return callable(RequestInterface, array): Promise\PromiseInterface Returns the best handler for the given system.
      *
      *
      * @throws \RuntimeException if no viable Handler is available.
      * @throws \RuntimeException if no viable Handler is available.
      */
      */
-    public static function chooseHandler(): callable
+    public static function chooseHandler(array $handlerOptions = []): callable
     {
     {
-        $handler = null;
+        $sharingMode = CurlShareHandleState::normalizeMode($handlerOptions['transport_sharing'] ?? null, 'transport_sharing');
+        $sharingRequired = self::isTransportSharingRequired($sharingMode);
+        $connectionCapsRequired = self::hasConnectionCapOptions($handlerOptions);
+        $handler = self::createCurlHandler($sharingMode, $handlerOptions);
 
 
-        if (\defined('CURLOPT_CUSTOMREQUEST') && \function_exists('curl_version') && version_compare(curl_version()['version'], '7.21.2') >= 0) {
-            if (\function_exists('curl_multi_exec') && \function_exists('curl_exec')) {
-                $handler = Proxy::wrapSync(new CurlMultiHandler(), new CurlHandler());
-            } elseif (\function_exists('curl_exec')) {
-                $handler = new CurlHandler();
-            } elseif (\function_exists('curl_multi_exec')) {
-                $handler = new CurlMultiHandler();
-            }
+        if ($sharingRequired && $handler === null) {
+            throw new \RuntimeException('Required transport sharing requires the PHP cURL extension, curl_exec() or curl_multi_exec(), and libcurl 7.21.2 or higher.');
         }
         }
 
 
         if (\ini_get('allow_url_fopen')) {
         if (\ini_get('allow_url_fopen')) {
-            $handler = $handler
-                ? Proxy::wrapStreaming($handler, new StreamHandler())
-                : new StreamHandler();
-        } elseif (!$handler) {
-            throw new \RuntimeException('GuzzleHttp requires cURL, the allow_url_fopen ini setting, or a custom HTTP handler.');
+            return self::addStreamHandler($handler, $sharingMode, $sharingRequired, self::connectionCapOptions($handlerOptions));
+        }
+
+        if ($handler !== null) {
+            return $handler;
+        }
+
+        if ($connectionCapsRequired) {
+            throw new \RuntimeException('Connection cap options require a cap-capable cURL multi handler or the allow_url_fopen ini setting for stream fallback.');
+        }
+
+        throw new \RuntimeException('GuzzleHttp requires cURL, the allow_url_fopen ini setting, or a custom HTTP handler.');
+    }
+
+    private static function isTransportSharingRequired(string $sharingMode): bool
+    {
+        return $sharingMode === TransportSharing::HANDLER_REQUIRE;
+    }
+
+    /**
+     * @param array{max_host_connections?: mixed, max_total_connections?: mixed} $handlerOptions
+     */
+    private static function hasConnectionCapOptions(array $handlerOptions): bool
+    {
+        return self::connectionCapOptions($handlerOptions) !== [];
+    }
+
+    /**
+     * @param array{max_host_connections?: mixed, max_total_connections?: mixed, multiplex?: mixed} $handlerOptions
+     *
+     * @return (callable(RequestInterface, array): Promise\PromiseInterface)|null
+     */
+    private static function createCurlHandler(string $sharingMode, array $handlerOptions): ?callable
+    {
+        if (!\defined('CURLOPT_CUSTOMREQUEST') || !CurlVersion::supportsCurlHandler()) {
+            return null;
+        }
+
+        $connectionCapOptions = self::connectionCapOptions($handlerOptions);
+        if ($connectionCapOptions !== [] && (!CurlVersion::supportsConnectionCaps() || !\function_exists('curl_multi_exec'))) {
+            return null;
+        }
+
+        $curlHandlerOptions = self::createCurlHandlerOptions($sharingMode);
+        $curlMultiHandlerOptions = $curlHandlerOptions + $connectionCapOptions;
+        if (($handlerOptions['multiplex'] ?? null) === Multiplexing::NONE) {
+            // Forwarded to the CurlMultiHandler only: CurlHandler and
+            // StreamHandler validate known options, and both satisfy NONE
+            // per-request without a handler option.
+            $curlMultiHandlerOptions['multiplex'] = Multiplexing::NONE;
+        }
+
+        if (\function_exists('curl_multi_exec') && \function_exists('curl_exec')) {
+            $multiHandler = new CurlMultiHandler($curlMultiHandlerOptions);
+
+            if ($connectionCapOptions !== []) {
+                // Connection caps only govern transfers on the multi handle, so
+                // the synchronous CurlHandler fast path would escape them.
+                return $multiHandler;
+            }
+
+            return Proxy::wrapSync($multiHandler, new CurlHandler($curlHandlerOptions));
+        }
+
+        if ($connectionCapOptions === [] && \function_exists('curl_exec')) {
+            return new CurlHandler($curlHandlerOptions);
         }
         }
 
 
-        return $handler;
+        if (\function_exists('curl_multi_exec')) {
+            return new CurlMultiHandler($curlMultiHandlerOptions);
+        }
+
+        return null;
+    }
+
+    /**
+     * @return array<string, mixed>
+     */
+    private static function createCurlHandlerOptions(string $sharingMode): array
+    {
+        if ($sharingMode === TransportSharing::NONE) {
+            return [];
+        }
+
+        $shareState = CurlShareHandleState::fromOption($sharingMode);
+
+        return $shareState === null ? [] : ['transport_sharing' => $shareState];
+    }
+
+    /**
+     * @param array{max_host_connections?: mixed, max_total_connections?: mixed} $handlerOptions
+     *
+     * @return array{max_host_connections?: int, max_total_connections?: int}
+     */
+    private static function connectionCapOptions(array $handlerOptions): array
+    {
+        $options = [];
+        foreach (['max_host_connections', 'max_total_connections'] as $capOption) {
+            $value = $handlerOptions[$capOption] ?? null;
+            if ($value === null) {
+                continue;
+            }
+
+            if (!\is_int($value) || $value < 1) {
+                throw new InvalidArgumentException(\sprintf('%s must be a positive integer.', $capOption));
+            }
+
+            $options[$capOption] = $value;
+        }
+
+        return $options;
+    }
+
+    /**
+     * @param (callable(RequestInterface, array): Promise\PromiseInterface)|null $handler
+     * @param array{max_host_connections?: int, max_total_connections?: int}     $connectionCapOptions
+     *
+     * @return callable(RequestInterface, array): Promise\PromiseInterface
+     */
+    private static function addStreamHandler(?callable $handler, string $sharingMode, bool $sharingRequired, array $connectionCapOptions): callable
+    {
+        $streamHandler = new StreamHandler(['transport_sharing' => $sharingMode] + $connectionCapOptions);
+
+        if ($handler === null) {
+            return $streamHandler;
+        }
+
+        if (!$sharingRequired) {
+            $handler = Proxy::wrapTlsFallback($handler, $streamHandler);
+        }
+
+        return Proxy::wrapStreaming($handler, $streamHandler);
     }
     }
 
 
     /**
     /**
@@ -133,6 +268,8 @@ final class Utils
      */
      */
     public static function defaultCaBundle(): string
     public static function defaultCaBundle(): string
     {
     {
+        \trigger_deprecation('guzzlehttp/guzzle', '7.1', '%s() is deprecated and will be removed in 8.0. This method is not needed in PHP 5.6+.', __METHOD__);
+
         static $cached = null;
         static $cached = null;
         static $cafiles = [
         static $cafiles = [
             // Red Hat, CentOS, Fedora (provided by the ca-certificates package)
             // Red Hat, CentOS, Fedora (provided by the ca-certificates package)
@@ -175,14 +312,14 @@ final class Utils
 No system CA bundle could be found in any of the the common system locations.
 No system CA bundle could be found in any of the the common system locations.
 PHP versions earlier than 5.6 are not properly configured to use the system's
 PHP versions earlier than 5.6 are not properly configured to use the system's
 CA bundle by default. In order to verify peer certificates, you will need to
 CA bundle by default. In order to verify peer certificates, you will need to
-supply the path on disk to a certificate bundle to the 'verify' request
-option: https://docs.guzzlephp.org/en/latest/request-options.html#verify. If
+supply the path on disk to a certificate bundle to the 'verify' request option:
+https://github.com/guzzle/guzzle/blob/7.15/docs/request-options.md#verify. If
 you do not need a specific certificate bundle, then Mozilla provides a commonly
 you do not need a specific certificate bundle, then Mozilla provides a commonly
 used CA bundle which can be downloaded here (provided by the maintainer of
 used CA bundle which can be downloaded here (provided by the maintainer of
-cURL): https://curl.haxx.se/ca/cacert.pem. Once you have a CA bundle available
-on disk, you can set the 'openssl.cafile' PHP ini setting to point to the path
-to the file, allowing you to omit the 'verify' request option. See
-https://curl.haxx.se/docs/sslcerts.html for more information.
+cURL): https://curl.se/ca/cacert.pem. Once you have a CA bundle available on
+disk, you can set the 'openssl.cafile' PHP ini setting to point to the path to
+the file, allowing you to omit the 'verify' request option. See
+https://curl.se/docs/sslcerts.html for more information.
 EOT
 EOT
         );
         );
     }
     }
@@ -195,28 +332,61 @@ EOT
     {
     {
         $result = [];
         $result = [];
         foreach (\array_keys($headers) as $key) {
         foreach (\array_keys($headers) as $key) {
-            $result[\strtolower($key)] = $key;
+            $result[Psr7\Utils::asciiToLower((string) $key)] = $key;
         }
         }
 
 
         return $result;
         return $result;
     }
     }
 
 
+    /**
+     * @param mixed $protocols
+     *
+     * @return string[]
+     *
+     * @throws InvalidArgumentException
+     */
+    public static function normalizeProtocols($protocols): array
+    {
+        if (!\is_array($protocols) || $protocols === []) {
+            throw new InvalidArgumentException('protocols must be a non-empty array of "http" and/or "https"');
+        }
+
+        $normalized = [];
+
+        foreach ($protocols as $protocol) {
+            if (!\is_string($protocol)) {
+                throw new InvalidArgumentException('protocols must contain only strings');
+            }
+
+            if ($protocol !== 'http' && $protocol !== 'https') {
+                throw new InvalidArgumentException('protocols may only contain "http" and "https"');
+            }
+
+            $normalized[$protocol] = true;
+        }
+
+        return \array_keys($normalized);
+    }
+
     /**
     /**
      * Returns true if the provided host matches any of the no proxy areas.
      * Returns true if the provided host matches any of the no proxy areas.
      *
      *
-     * This method will strip a port from the host if it is present. Each pattern
-     * can be matched with an exact match (e.g., "foo.com" == "foo.com") or a
-     * partial match: (e.g., "foo.com" == "baz.foo.com" and ".foo.com" ==
-     * "baz.foo.com", but ".foo.com" != "foo.com").
+     * This method will strip a port from the host if it is present. Domain
+     * patterns are matched case-insensitively. Exact IP literal patterns are
+     * matched by their normalized binary address.
      *
      *
      * Areas are matched in the following cases:
      * Areas are matched in the following cases:
      * 1. "*" (without quotes) always matches any hosts.
      * 1. "*" (without quotes) always matches any hosts.
-     * 2. An exact match.
-     * 3. The area starts with "." and the area is the last part of the host. e.g.
+     * 2. An exact domain or IP literal match.
+     * 3. A bare domain matches itself and its subdomains. e.g. 'mit.edu' will
+     *    match 'mit.edu' and 'foo.mit.edu'.
+     * 4. The area starts with "." and the area is the last part of the host. e.g.
      *    '.mit.edu' will match any host that ends with '.mit.edu'.
      *    '.mit.edu' will match any host that ends with '.mit.edu'.
+     * 5. IP CIDR entries match IP literal hosts. e.g. '192.168.0.0/16' will
+     *    match '192.168.1.10' and 'fd00::/8' will match '[fd00::1]'.
      *
      *
      * @param string   $host         Host to check against the patterns.
      * @param string   $host         Host to check against the patterns.
-     * @param string[] $noProxyArray An array of host patterns.
+     * @param string[] $noProxyArray An array of host or CIDR patterns.
      *
      *
      * @throws InvalidArgumentException
      * @throws InvalidArgumentException
      */
      */
@@ -226,28 +396,64 @@ EOT
             throw new InvalidArgumentException('Empty host provided');
             throw new InvalidArgumentException('Empty host provided');
         }
         }
 
 
-        // Strip port if present.
-        [$host] = \explode(':', $host, 2);
+        $target = self::parseNoProxyHostString($host);
+        if ($target === null) {
+            return false;
+        }
+
+        return self::matchesNoProxyList($target, $noProxyArray);
+    }
 
 
-        foreach ($noProxyArray as $area) {
-            // Always match on wildcards.
-            if ($area === '*') {
-                return true;
-            }
+    /**
+     * Returns true if the provided URI matches any of the no proxy areas.
+     *
+     * Matching follows the same rules as isHostInNoProxy(), with the
+     * addition that areas may carry a port (e.g. "example.com:8080" or
+     * "[::1]:8080") which is compared against the URI port (or the scheme
+     * default port when the URI has none).
+     *
+     * @param mixed $noProxy No-proxy host, host-and-port, or CIDR patterns.
+     *
+     * @internal
+     */
+    public static function isUriInNoProxy(UriInterface $uri, $noProxy): bool
+    {
+        if (\is_string($noProxy)) {
+            $noProxy = \explode(',', $noProxy);
+        }
+
+        if (!\is_array($noProxy)) {
+            return false;
+        }
+
+        $target = self::parseNoProxyTarget($uri);
+        if ($target === null) {
+            return false;
+        }
 
 
-            if (empty($area)) {
-                // Don't match on empty values.
+        return self::matchesNoProxyList($target, $noProxy);
+    }
+
+    /**
+     * @param array{type: string, value: string, port: int|null, matchesRoot: bool} $target
+     * @param array<array-key, mixed>                                               $noProxy
+     */
+    private static function matchesNoProxyList(array $target, array $noProxy): bool
+    {
+        foreach ($noProxy as $area) {
+            if (!\is_string($area)) {
                 continue;
                 continue;
             }
             }
 
 
-            if ($area === $host) {
-                // Exact matches.
+            $area = \trim($area, " \n\r\t\0\x0B");
+
+            // Always match on wildcards.
+            if ($area === '*') {
                 return true;
                 return true;
             }
             }
-            // Special match if the area when prefixed with ".". Remove any
-            // existing leading "." and add a new leading ".".
-            $area = '.'.\ltrim($area, '.');
-            if (\substr($host, -\strlen($area)) === $area) {
+
+            $rule = self::parseNoProxyRule($area);
+            if ($rule !== null && self::noProxyRuleMatches($target, $rule)) {
                 return true;
                 return true;
             }
             }
         }
         }
@@ -255,6 +461,315 @@ EOT
         return false;
         return false;
     }
     }
 
 
+    /**
+     * @return array{type: string, value: string, port: int|null, matchesRoot: bool}|null
+     */
+    private static function parseNoProxyTarget(UriInterface $uri): ?array
+    {
+        $host = $uri->getHost();
+        if ($host === '') {
+            return null;
+        }
+
+        return self::parseNoProxyHost($host, $uri->getPort() ?? self::getDefaultPort($uri->getScheme()), true);
+    }
+
+    /**
+     * @return array{type: string, value: string, port: int|null, matchesRoot: bool}|null
+     */
+    private static function parseNoProxyHostString(string $host): ?array
+    {
+        $hostAndPort = self::splitNoProxyHostAndPort($host);
+        if ($hostAndPort === null) {
+            return null;
+        }
+
+        [$host] = $hostAndPort;
+
+        return self::parseNoProxyHost($host, null, true);
+    }
+
+    /**
+     * @return array{type: string, value: string, port: int|null, matchesRoot: bool}|array{type: string, value: string, prefix: int}|null
+     */
+    private static function parseNoProxyRule(string $area): ?array
+    {
+        $area = \trim($area, " \n\r\t\0\x0B");
+        if ($area === '' || $area === '*') {
+            return null;
+        }
+
+        if (\strpos($area, '/') !== false) {
+            return self::parseNoProxyCidrRule($area);
+        }
+
+        $matchesRoot = true;
+        if ($area[0] === '.') {
+            $matchesRoot = false;
+            $area = \substr($area, 1);
+        }
+
+        $hostAndPort = self::splitNoProxyHostAndPort($area);
+        if ($hostAndPort === null) {
+            return null;
+        }
+
+        [$host, $port] = $hostAndPort;
+
+        if ($host === '*') {
+            if (!$matchesRoot) {
+                return null;
+            }
+
+            return [
+                'type' => 'wildcard',
+                'value' => '*',
+                'port' => $port,
+                'matchesRoot' => true,
+            ];
+        }
+
+        $rule = self::parseNoProxyHost($host, $port, $matchesRoot);
+        if ($rule !== null && !$matchesRoot && $rule['type'] === 'ip') {
+            return null;
+        }
+
+        return $rule;
+    }
+
+    /**
+     * @return array{type: string, value: string, port: int|null, matchesRoot: bool}|null
+     */
+    private static function parseNoProxyHost(string $host, ?int $port, bool $matchesRoot): ?array
+    {
+        if ($host !== '' && $host[0] === '[') {
+            if (\substr($host, -1) !== ']') {
+                return null;
+            }
+
+            $address = \substr($host, 1, -1);
+            if (!\filter_var($address, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) {
+                return null;
+            }
+
+            $host = $address;
+        }
+
+        $packedIp = self::packIpAddress($host);
+        if ($packedIp !== false) {
+            return [
+                'type' => 'ip',
+                'value' => $packedIp,
+                'port' => $port,
+                'matchesRoot' => $matchesRoot,
+            ];
+        }
+
+        if ($host === '' || \strpos($host, ':') !== false) {
+            return null;
+        }
+
+        // Normalize a single DNS root dot for no-proxy domain matching.
+        if (\substr($host, -1) === '.') {
+            $host = \substr($host, 0, -1);
+            if ($host === '') {
+                return null;
+            }
+        }
+
+        return [
+            'type' => 'domain',
+            'value' => Psr7\Utils::asciiToLower($host),
+            'port' => $port,
+            'matchesRoot' => $matchesRoot,
+        ];
+    }
+
+    /**
+     * @return array{0: string, 1: int|null}|null
+     */
+    private static function splitNoProxyHostAndPort(string $area): ?array
+    {
+        if ($area !== '' && $area[0] === '[') {
+            $closingBracket = \strpos($area, ']');
+            if ($closingBracket === false) {
+                return null;
+            }
+
+            $host = \substr($area, 0, $closingBracket + 1);
+            $tail = \substr($area, $closingBracket + 1);
+            if ($tail === '') {
+                return [$host, null];
+            }
+
+            if ($tail[0] !== ':') {
+                return null;
+            }
+
+            $port = self::parseNoProxyPort(\substr($tail, 1));
+
+            return $port === null ? null : [$host, $port];
+        }
+
+        if (self::packIpAddress($area) !== false) {
+            return [$area, null];
+        }
+
+        $colon = \strrpos($area, ':');
+        if ($colon === false) {
+            return [$area, null];
+        }
+
+        $port = self::parseNoProxyPort(\substr($area, $colon + 1));
+        if ($port === null) {
+            return null;
+        }
+
+        return [\substr($area, 0, $colon), $port];
+    }
+
+    private static function parseNoProxyPort(string $port): ?int
+    {
+        return self::parseBoundedUnsignedInteger($port, 65535);
+    }
+
+    /**
+     * @return array{type: string, value: string, prefix: int}|null
+     */
+    private static function parseNoProxyCidrRule(string $area): ?array
+    {
+        $slash = \strpos($area, '/');
+        if ($slash === false) {
+            return null;
+        }
+
+        $prefix = \substr($area, $slash + 1);
+
+        $network = \substr($area, 0, $slash);
+        if ($network !== '' && $network[0] === '[' && \substr($network, -1) === ']') {
+            $network = \substr($network, 1, -1);
+        }
+
+        $network = self::packIpAddress($network);
+        if ($network === false) {
+            return null;
+        }
+
+        $prefix = self::parseBoundedUnsignedInteger($prefix, \strlen($network) * 8);
+        if ($prefix === null) {
+            return null;
+        }
+
+        return [
+            'type' => 'cidr',
+            'value' => $network,
+            'prefix' => $prefix,
+        ];
+    }
+
+    private static function parseBoundedUnsignedInteger(string $value, int $max): ?int
+    {
+        if ($value === '' || !\ctype_digit($value)) {
+            return null;
+        }
+
+        $normalized = \ltrim($value, '0');
+        $normalized = $normalized === '' ? '0' : $normalized;
+        $limit = (string) $max;
+
+        if (\strlen($normalized) > \strlen($limit) || (\strlen($normalized) === \strlen($limit) && \strcmp($normalized, $limit) > 0)) {
+            return null;
+        }
+
+        return (int) $normalized;
+    }
+
+    /**
+     * @param array{type: string, value: string, port: int|null, matchesRoot: bool}                      $target
+     * @param array{type: string, value: string, port?: int|null, matchesRoot?: bool, prefix?: int|null} $rule
+     */
+    private static function noProxyRuleMatches(array $target, array $rule): bool
+    {
+        if ($rule['type'] === 'wildcard') {
+            return ($rule['port'] ?? null) === null || $rule['port'] === $target['port'];
+        }
+
+        if ($rule['type'] === 'cidr') {
+            if ($target['type'] !== 'ip' || !isset($rule['prefix'])) {
+                return false;
+            }
+
+            if (\strlen($target['value']) !== \strlen($rule['value'])) {
+                return false;
+            }
+
+            return self::ipMatchesPrefix($target['value'], $rule['value'], $rule['prefix']);
+        }
+
+        if (($rule['port'] ?? null) !== null && $rule['port'] !== $target['port']) {
+            return false;
+        }
+
+        if ($rule['type'] !== $target['type']) {
+            return false;
+        }
+
+        if ($rule['type'] === 'ip') {
+            return $rule['value'] === $target['value'];
+        }
+
+        if (($rule['matchesRoot'] ?? false) && $target['value'] === $rule['value']) {
+            return true;
+        }
+
+        $suffix = '.'.$rule['value'];
+
+        return \substr($target['value'], -\strlen($suffix)) === $suffix;
+    }
+
+    /**
+     * @return string|false
+     */
+    private static function packIpAddress(string $ip)
+    {
+        if (!\filter_var($ip, \FILTER_VALIDATE_IP)) {
+            return false;
+        }
+
+        return \inet_pton($ip);
+    }
+
+    private static function ipMatchesPrefix(string $address, string $network, int $prefix): bool
+    {
+        $fullBytes = \intdiv($prefix, 8);
+        $remainingBits = $prefix % 8;
+
+        if ($fullBytes > 0 && \substr($address, 0, $fullBytes) !== \substr($network, 0, $fullBytes)) {
+            return false;
+        }
+
+        if ($remainingBits === 0) {
+            return true;
+        }
+
+        $mask = (0xFF << (8 - $remainingBits)) & 0xFF;
+
+        return (\ord($address[$fullBytes]) & $mask) === (\ord($network[$fullBytes]) & $mask);
+    }
+
+    private static function getDefaultPort(string $scheme): ?int
+    {
+        if ($scheme === 'http') {
+            return 80;
+        }
+
+        if ($scheme === 'https') {
+            return 443;
+        }
+
+        return null;
+    }
+
     /**
     /**
      * Wrapper for json_decode that throws when an error occurs.
      * Wrapper for json_decode that throws when an error occurs.
      *
      *
@@ -269,9 +784,16 @@ EOT
      * @throws InvalidArgumentException if the JSON cannot be decoded.
      * @throws InvalidArgumentException if the JSON cannot be decoded.
      *
      *
      * @see https://www.php.net/manual/en/function.json-decode.php
      * @see https://www.php.net/manual/en/function.json-decode.php
+     * @deprecated Utils::jsonDecode() will be removed in guzzlehttp/guzzle:8.0. Use PHP's json_decode() instead.
      */
      */
     public static function jsonDecode(string $json, bool $assoc = false, int $depth = 512, int $options = 0)
     public static function jsonDecode(string $json, bool $assoc = false, int $depth = 512, int $options = 0)
     {
     {
+        \trigger_deprecation('guzzlehttp/guzzle', '7.15', '%s() is deprecated and will be removed in 8.0. Use PHP\'s json_decode() instead.', __METHOD__);
+
+        if ($depth < 1) {
+            throw new InvalidArgumentException('json_decode error: Maximum stack depth exceeded');
+        }
+
         $data = \json_decode($json, $assoc, $depth, $options);
         $data = \json_decode($json, $assoc, $depth, $options);
         if (\JSON_ERROR_NONE !== \json_last_error()) {
         if (\JSON_ERROR_NONE !== \json_last_error()) {
             throw new InvalidArgumentException('json_decode error: '.\json_last_error_msg());
             throw new InvalidArgumentException('json_decode error: '.\json_last_error_msg());
@@ -290,9 +812,12 @@ EOT
      * @throws InvalidArgumentException if the JSON cannot be encoded.
      * @throws InvalidArgumentException if the JSON cannot be encoded.
      *
      *
      * @see https://www.php.net/manual/en/function.json-encode.php
      * @see https://www.php.net/manual/en/function.json-encode.php
+     * @deprecated Utils::jsonEncode() will be removed in guzzlehttp/guzzle:8.0. Use PHP's json_encode() instead.
      */
      */
     public static function jsonEncode($value, int $options = 0, int $depth = 512): string
     public static function jsonEncode($value, int $options = 0, int $depth = 512): string
     {
     {
+        \trigger_deprecation('guzzlehttp/guzzle', '7.15', '%s() is deprecated and will be removed in 8.0. Use PHP\'s json_encode() instead.', __METHOD__);
+
         $json = \json_encode($value, $options, $depth);
         $json = \json_encode($value, $options, $depth);
         if (\JSON_ERROR_NONE !== \json_last_error()) {
         if (\JSON_ERROR_NONE !== \json_last_error()) {
             throw new InvalidArgumentException('json_encode error: '.\json_last_error_msg());
             throw new InvalidArgumentException('json_encode error: '.\json_last_error_msg());
@@ -315,6 +840,39 @@ EOT
         return (float) \function_exists('hrtime') ? \hrtime(true) / 1e9 : \microtime(true);
         return (float) \function_exists('hrtime') ? \hrtime(true) / 1e9 : \microtime(true);
     }
     }
 
 
+    /**
+     * @param mixed $value
+     *
+     * @internal
+     */
+    public static function normalizeIdnConversionOption($value): ?int
+    {
+        if ($value === null || $value === false) {
+            return null;
+        }
+
+        if ($value === true) {
+            return \IDNA_DEFAULT;
+        }
+
+        if (\is_int($value)) {
+            return $value;
+        }
+
+        if ((\is_string($value) && \is_numeric($value)) || (\is_float($value) && \is_finite($value))) {
+            \trigger_deprecation(
+                'guzzlehttp/guzzle',
+                '7.11',
+                'Passing %s as the "idn_conversion" request option is deprecated; guzzlehttp/guzzle 8.0 will reject values that are not true, false, null, or an integer IDNA_* bitmask.',
+                \get_debug_type($value)
+            );
+
+            return (int) $value;
+        }
+
+        throw new InvalidArgumentException('idn_conversion must be true, false, null, or an integer IDNA_* bitmask');
+    }
+
     /**
     /**
      * @throws InvalidArgumentException
      * @throws InvalidArgumentException
      *
      *

+ 108 - 7
modules/boonex/xero/plugins/guzzlehttp/guzzle/src/functions.php

@@ -11,11 +11,26 @@ namespace GuzzleHttp;
  * @return string Returns a string containing the type of the variable and
  * @return string Returns a string containing the type of the variable and
  *                if a class is provided, the class name.
  *                if a class is provided, the class name.
  *
  *
- * @deprecated describe_type will be removed in guzzlehttp/guzzle:8.0. Use Utils::describeType instead.
+ * @deprecated describe_type will be removed in guzzlehttp/guzzle:8.0. Use get_debug_type() instead.
  */
  */
 function describe_type($input): string
 function describe_type($input): string
 {
 {
-    return Utils::describeType($input);
+    \trigger_deprecation('guzzlehttp/guzzle', '7.1', '%s() is deprecated and will be removed in 8.0. Use get_debug_type() instead.', __FUNCTION__);
+
+    switch (\gettype($input)) {
+        case 'object':
+            return 'object('.\get_class($input).')';
+        case 'array':
+            return 'array('.\count($input).')';
+        default:
+            \ob_start();
+            \var_dump($input);
+            // normalize float vs double
+            /** @var string $varDumpContent */
+            $varDumpContent = \ob_get_clean();
+
+            return \str_replace('double(', 'float(', \rtrim($varDumpContent, " \n\r\t\0\x0B"));
+    }
 }
 }
 
 
 /**
 /**
@@ -28,6 +43,8 @@ function describe_type($input): string
  */
  */
 function headers_from_lines(iterable $lines): array
 function headers_from_lines(iterable $lines): array
 {
 {
+    \trigger_deprecation('guzzlehttp/guzzle', '7.1', '%s() is deprecated and will be removed in 8.0. Use Utils::headersFromLines() instead.', __FUNCTION__);
+
     return Utils::headersFromLines($lines);
     return Utils::headersFromLines($lines);
 }
 }
 
 
@@ -42,6 +59,8 @@ function headers_from_lines(iterable $lines): array
  */
  */
 function debug_resource($value = null)
 function debug_resource($value = null)
 {
 {
+    \trigger_deprecation('guzzlehttp/guzzle', '7.1', '%s() is deprecated and will be removed in 8.0. Use Utils::debugResource() instead.', __FUNCTION__);
+
     return Utils::debugResource($value);
     return Utils::debugResource($value);
 }
 }
 
 
@@ -58,6 +77,8 @@ function debug_resource($value = null)
  */
  */
 function choose_handler(): callable
 function choose_handler(): callable
 {
 {
+    \trigger_deprecation('guzzlehttp/guzzle', '7.1', '%s() is deprecated and will be removed in 8.0. Use Utils::chooseHandler() instead.', __FUNCTION__);
+
     return Utils::chooseHandler();
     return Utils::chooseHandler();
 }
 }
 
 
@@ -68,6 +89,8 @@ function choose_handler(): callable
  */
  */
 function default_user_agent(): string
 function default_user_agent(): string
 {
 {
+    \trigger_deprecation('guzzlehttp/guzzle', '7.1', '%s() is deprecated and will be removed in 8.0. Use Utils::defaultUserAgent() instead.', __FUNCTION__);
+
     return Utils::defaultUserAgent();
     return Utils::defaultUserAgent();
 }
 }
 
 
@@ -88,7 +111,60 @@ function default_user_agent(): string
  */
  */
 function default_ca_bundle(): string
 function default_ca_bundle(): string
 {
 {
-    return Utils::defaultCaBundle();
+    \trigger_deprecation('guzzlehttp/guzzle', '7.1', '%s() is deprecated and will be removed in 8.0. This function is not needed in PHP 5.6+.', __FUNCTION__);
+
+    static $cached = null;
+    static $cafiles = [
+        // Red Hat, CentOS, Fedora (provided by the ca-certificates package)
+        '/etc/pki/tls/certs/ca-bundle.crt',
+        // Ubuntu, Debian (provided by the ca-certificates package)
+        '/etc/ssl/certs/ca-certificates.crt',
+        // FreeBSD (provided by the ca_root_nss package)
+        '/usr/local/share/certs/ca-root-nss.crt',
+        // SLES 12 (provided by the ca-certificates package)
+        '/var/lib/ca-certificates/ca-bundle.pem',
+        // OS X provided by homebrew (using the default path)
+        '/usr/local/etc/openssl/cert.pem',
+        // Google app engine
+        '/etc/ca-certificates.crt',
+        // Windows?
+        'C:\\windows\\system32\\curl-ca-bundle.crt',
+        'C:\\windows\\curl-ca-bundle.crt',
+    ];
+
+    if ($cached) {
+        return $cached;
+    }
+
+    if ($ca = \ini_get('openssl.cafile')) {
+        return $cached = $ca;
+    }
+
+    if ($ca = \ini_get('curl.cainfo')) {
+        return $cached = $ca;
+    }
+
+    foreach ($cafiles as $filename) {
+        if (\file_exists($filename)) {
+            return $cached = $filename;
+        }
+    }
+
+    throw new \RuntimeException(
+        <<< EOT
+No system CA bundle could be found in any of the the common system locations.
+PHP versions earlier than 5.6 are not properly configured to use the system's
+CA bundle by default. In order to verify peer certificates, you will need to
+supply the path on disk to a certificate bundle to the 'verify' request option:
+https://github.com/guzzle/guzzle/blob/7.15/docs/request-options.md#verify. If
+you do not need a specific certificate bundle, then Mozilla provides a commonly
+used CA bundle which can be downloaded here (provided by the maintainer of
+cURL): https://curl.se/ca/cacert.pem. Once you have a CA bundle available on
+disk, you can set the 'openssl.cafile' PHP ini setting to point to the path to
+the file, allowing you to omit the 'verify' request option. See
+https://curl.se/docs/sslcerts.html for more information.
+EOT
+    );
 }
 }
 
 
 /**
 /**
@@ -99,6 +175,8 @@ function default_ca_bundle(): string
  */
  */
 function normalize_header_keys(array $headers): array
 function normalize_header_keys(array $headers): array
 {
 {
+    \trigger_deprecation('guzzlehttp/guzzle', '7.1', '%s() is deprecated and will be removed in 8.0. Use Utils::normalizeHeaderKeys() instead.', __FUNCTION__);
+
     return Utils::normalizeHeaderKeys($headers);
     return Utils::normalizeHeaderKeys($headers);
 }
 }
 
 
@@ -125,6 +203,8 @@ function normalize_header_keys(array $headers): array
  */
  */
 function is_host_in_noproxy(string $host, array $noProxyArray): bool
 function is_host_in_noproxy(string $host, array $noProxyArray): bool
 {
 {
+    \trigger_deprecation('guzzlehttp/guzzle', '7.1', '%s() is deprecated and will be removed in 8.0. Use Utils::isHostInNoProxy() instead.', __FUNCTION__);
+
     return Utils::isHostInNoProxy($host, $noProxyArray);
     return Utils::isHostInNoProxy($host, $noProxyArray);
 }
 }
 
 
@@ -142,11 +222,23 @@ function is_host_in_noproxy(string $host, array $noProxyArray): bool
  * @throws Exception\InvalidArgumentException if the JSON cannot be decoded.
  * @throws Exception\InvalidArgumentException if the JSON cannot be decoded.
  *
  *
  * @see https://www.php.net/manual/en/function.json-decode.php
  * @see https://www.php.net/manual/en/function.json-decode.php
- * @deprecated json_decode will be removed in guzzlehttp/guzzle:8.0. Use Utils::jsonDecode instead.
+ * @deprecated json_decode will be removed in guzzlehttp/guzzle:8.0. Use PHP's json_decode() instead.
  */
  */
 function json_decode(string $json, bool $assoc = false, int $depth = 512, int $options = 0)
 function json_decode(string $json, bool $assoc = false, int $depth = 512, int $options = 0)
 {
 {
-    return Utils::jsonDecode($json, $assoc, $depth, $options);
+    \trigger_deprecation('guzzlehttp/guzzle', '7.1', '%s() is deprecated and will be removed in 8.0. Use PHP\'s json_decode() instead.', __FUNCTION__);
+
+    if ($depth < 1) {
+        throw new Exception\InvalidArgumentException('json_decode error: Maximum stack depth exceeded');
+    }
+
+    $data = \json_decode($json, $assoc, $depth, $options);
+    if (\JSON_ERROR_NONE !== \json_last_error()) {
+        throw new Exception\InvalidArgumentException('json_decode error: '.\json_last_error_msg());
+    }
+
+    /** @var object|array|string|int|float|bool|null $data */
+    return $data;
 }
 }
 
 
 /**
 /**
@@ -159,9 +251,18 @@ function json_decode(string $json, bool $assoc = false, int $depth = 512, int $o
  * @throws Exception\InvalidArgumentException if the JSON cannot be encoded.
  * @throws Exception\InvalidArgumentException if the JSON cannot be encoded.
  *
  *
  * @see https://www.php.net/manual/en/function.json-encode.php
  * @see https://www.php.net/manual/en/function.json-encode.php
- * @deprecated json_encode will be removed in guzzlehttp/guzzle:8.0. Use Utils::jsonEncode instead.
+ * @deprecated json_encode will be removed in guzzlehttp/guzzle:8.0. Use PHP's json_encode() instead.
  */
  */
 function json_encode($value, int $options = 0, int $depth = 512): string
 function json_encode($value, int $options = 0, int $depth = 512): string
 {
 {
-    return Utils::jsonEncode($value, $options, $depth);
+    \trigger_deprecation('guzzlehttp/guzzle', '7.1', '%s() is deprecated and will be removed in 8.0. Use PHP\'s json_encode() instead.', __FUNCTION__);
+
+    /** @var positive-int $depth */
+    $json = \json_encode($value, $options, $depth);
+    if (\JSON_ERROR_NONE !== \json_last_error()) {
+        throw new Exception\InvalidArgumentException('json_encode error: '.\json_last_error_msg());
+    }
+
+    /** @var non-empty-string $json */
+    return $json;
 }
 }

+ 57 - 0
modules/boonex/xero/plugins/guzzlehttp/promises/CHANGELOG.md

@@ -1,6 +1,63 @@
 # CHANGELOG
 # CHANGELOG
 
 
 
 
+## 2.5.3 - 2026-08-24
+
+### Added
+
+- Added support for PHP 8.6
+
+
+## 2.5.2 - 2026-08-05
+
+### Fixed
+
+- Fixed `EachPromise` abandoning its aggregate when the pending window drains unsettled
+- Fixed `EachPromise` admitting new work after its aggregate has settled
+
+
+## 2.5.1 - 2026-07-08
+
+### Fixed
+
+- Fixed recursive `Utils::all()` rejecting generator inputs
+
+
+## 2.5.0 - 2026-06-02
+
+### Deprecated
+
+- Deprecated passing non-iterable inputs to promise collection helpers and `EachPromise`
+
+
+## 2.4.1 - 2026-05-20
+
+### Fixed
+
+- Fixed cancelling settled coroutines when no current promise remains
+
+
+## 2.4.0 - 2026-05-20
+
+### Changed
+
+- Empty `EachPromise` instances now resolve when the task queue runs without `wait()`
+
+
+## 2.3.1 - 2026-05-19
+
+### Fixed
+
+- Fixed `Utils::inspect()` returning the internal reason array instead of the `AggregateException`
+
+
+## 2.3.0 - 2025-08-22
+
+### Added
+
+- PHP 8.5 support
+
+
 ## 2.2.0 - 2025-03-27
 ## 2.2.0 - 2025-03-27
 
 
 ### Fixed
 ### Fixed

+ 18 - 41
modules/boonex/xero/plugins/guzzlehttp/promises/README.md

@@ -41,7 +41,7 @@ composer require guzzlehttp/promises
 | Version | Status              | PHP Version  |
 | Version | Status              | PHP Version  |
 |---------|---------------------|--------------|
 |---------|---------------------|--------------|
 | 1.x     | Security fixes only | >=5.5,<8.3   |
 | 1.x     | Security fixes only | >=5.5,<8.3   |
-| 2.x     | Latest              | >=7.2.5,<8.5 |
+| 2.x     | Latest              | >=7.2.5,<8.7 |
 
 
 
 
 ## Quick Start
 ## Quick Start
@@ -53,7 +53,7 @@ why the promise cannot be fulfilled.
 
 
 ### Callbacks
 ### Callbacks
 
 
-Callbacks are registered with the `then` method by providing an optional 
+Callbacks are registered with the `then` method by providing an optional
 `$onFulfilled` followed by an optional `$onRejected` function.
 `$onFulfilled` followed by an optional `$onRejected` function.
 
 
 
 
@@ -227,8 +227,8 @@ $promise = new Promise(function () use (&$promise) {
 echo $promise->wait(); // outputs "foo"
 echo $promise->wait(); // outputs "foo"
 ```
 ```
 
 
-If an exception is encountered while invoking the wait function of a promise,
-the promise is rejected with the exception and the exception is thrown.
+If a throwable is encountered while invoking the wait function of a promise,
+the promise is rejected with the throwable and the throwable is thrown.
 
 
 ```php
 ```php
 $promise = new Promise(function () use (&$promise) {
 $promise = new Promise(function () use (&$promise) {
@@ -247,8 +247,8 @@ $promise->resolve('foo');
 echo $promise->wait(); // outputs "foo"
 echo $promise->wait(); // outputs "foo"
 ```
 ```
 
 
-Calling `wait` on a promise that has been rejected will throw an exception. If
-the rejection reason is an instance of `\Exception` the reason is thrown.
+Calling `wait` on a promise that has been rejected will throw. If the rejection
+reason is an instance of `\Throwable` the reason is thrown.
 Otherwise, a `GuzzleHttp\Promise\RejectionException` is thrown and the reason
 Otherwise, a `GuzzleHttp\Promise\RejectionException` is thrown and the reason
 can be obtained by calling the `getReason` method of the exception.
 can be obtained by calling the `getReason` method of the exception.
 
 
@@ -323,9 +323,11 @@ assert('waited' === $promise->wait());
 
 
 A promise has the following methods:
 A promise has the following methods:
 
 
-- `then(callable $onFulfilled, callable $onRejected) : PromiseInterface`
-  
-  Appends fulfillment and rejection handlers to the promise, and returns a new promise resolving to the return value of the called handler.
+- `then(?callable $onFulfilled = null, ?callable $onRejected = null) : PromiseInterface`
+
+  Appends fulfillment and rejection handlers to the promise, and returns a new
+  promise resolving to the return value of the called handler. If a handler is
+  omitted, the original fulfillment value or rejection reason is forwarded.
 
 
 - `otherwise(callable $onRejected) : PromiseInterface`
 - `otherwise(callable $onRejected) : PromiseInterface`
   
   
@@ -438,11 +440,13 @@ $queue = GuzzleHttp\Promise\Utils::queue();
 $queue->run();
 $queue->run();
 ```
 ```
 
 
-For example, you could use Guzzle promises with React using a periodic timer:
+For example, you could use Guzzle promises with React using a short periodic
+timer. Avoid zero-interval timers because they may keep the loop busy even when
+there is no promise work to run.
 
 
 ```php
 ```php
 $loop = React\EventLoop\Factory::create();
 $loop = React\EventLoop\Factory::create();
-$loop->addPeriodicTimer(0, [$queue, 'run']);
+$loop->addPeriodicTimer(0.01, [$queue, 'run']);
 ```
 ```
 
 
 
 
@@ -510,36 +514,9 @@ $promise->resolve('foo');
 ```
 ```
 
 
 
 
-## Upgrading from Function API
-
-A static API was first introduced in 1.4.0, in order to mitigate problems with
-functions conflicting between global and local copies of the package. The
-function API was removed in 2.0.0. A migration table has been provided here for
-your convenience:
-
-| Original Function | Replacement Method |
-|----------------|----------------|
-| `queue` | `Utils::queue` |
-| `task` | `Utils::task` |
-| `promise_for` | `Create::promiseFor` |
-| `rejection_for` | `Create::rejectionFor` |
-| `exception_for` | `Create::exceptionFor` |
-| `iter_for` | `Create::iterFor` |
-| `inspect` | `Utils::inspect` |
-| `inspect_all` | `Utils::inspectAll` |
-| `unwrap` | `Utils::unwrap` |
-| `all` | `Utils::all` |
-| `some` | `Utils::some` |
-| `any` | `Utils::any` |
-| `settle` | `Utils::settle` |
-| `each` | `Each::of` |
-| `each_limit` | `Each::ofLimit` |
-| `each_limit_all` | `Each::ofLimitAll` |
-| `!is_fulfilled` | `Is::pending` |
-| `is_fulfilled` | `Is::fulfilled` |
-| `is_rejected` | `Is::rejected` |
-| `is_settled` | `Is::settled` |
-| `coroutine` | `Coroutine::of` |
+## Upgrading
+
+See [UPGRADING.md](UPGRADING.md) for package upgrade notes.
 
 
 
 
 ## Security
 ## Security

+ 83 - 0
modules/boonex/xero/plugins/guzzlehttp/promises/UPGRADING.md

@@ -0,0 +1,83 @@
+Guzzle Promises Upgrade Guide
+=============================
+
+1.x to 2.0
+----------
+
+Guzzle Promises 2.0 is a major release that removes deprecated APIs, raises the
+minimum PHP version, and adds PHP 7 parameter and return types. Applications that
+only use the object-oriented API should usually need small changes. Applications
+that call helper functions, implement package interfaces, extend package classes,
+or pass invalid argument types need closer review.
+
+#### PHP Version and Dependencies
+
+Guzzle Promises 2.0 requires PHP `^7.2.5 || ^8.0`. Guzzle Promises 1.x
+supported PHP `>=5.5`.
+
+#### PHP 7 Type Hints and Return Types
+
+Type hints and return types were added wherever possible. Please make sure:
+
+- You pass values of the documented type when calling methods and functions.
+- Classes that implement `PromiseInterface`, `PromisorInterface`, or
+  `TaskQueueInterface` update method signatures to remain compatible.
+- Classes that extend Guzzle Promises classes update any overridden method
+  signatures to remain compatible.
+- Code that expected package-specific exceptions for invalid argument types may
+  now receive PHP `TypeError` exceptions instead.
+
+#### Soft-Final Classes
+
+All previously non-final non-exception classes are now final or annotated with
+`@final`. If your code extends one of these classes, replace inheritance with
+composition or implement the relevant interface directly.
+
+#### Removed Function API
+
+The static API was introduced in 1.4.0 to mitigate problems with functions
+conflicting between global and local copies of the package. The function API was
+removed in 2.0.0, along with the Composer `files` autoload entry that loaded
+`src/functions_include.php`.
+
+Replace namespaced function calls with the corresponding static methods in the
+`GuzzleHttp\Promise` namespace:
+
+```php
+// Before:
+use function GuzzleHttp\Promise\promise_for;
+
+$promise = promise_for('value');
+
+// After:
+use GuzzleHttp\Promise\Create;
+
+$promise = Create::promiseFor('value');
+```
+
+| Original Function | Replacement Method |
+|-------------------|--------------------|
+| `queue` | `Utils::queue` |
+| `task` | `Utils::task` |
+| `promise_for` | `Create::promiseFor` |
+| `rejection_for` | `Create::rejectionFor` |
+| `exception_for` | `Create::exceptionFor` |
+| `iter_for` | `Create::iterFor` |
+| `inspect` | `Utils::inspect` |
+| `inspect_all` | `Utils::inspectAll` |
+| `unwrap` | `Utils::unwrap` |
+| `all` | `Utils::all` |
+| `some` | `Utils::some` |
+| `any` | `Utils::any` |
+| `settle` | `Utils::settle` |
+| `each` | `Each::of` |
+| `each_limit` | `Each::ofLimit` |
+| `each_limit_all` | `Each::ofLimitAll` |
+| `!is_fulfilled` | `Is::pending` |
+| `is_fulfilled` | `Is::fulfilled` |
+| `is_rejected` | `Is::rejected` |
+| `is_settled` | `Is::settled` |
+| `coroutine` | `Coroutine::of` |
+
+For the full 2.0 diff, see
+https://github.com/guzzle/promises/compare/1.5.3...2.0.0.

+ 12 - 9
modules/boonex/xero/plugins/guzzlehttp/promises/composer.json

@@ -1,8 +1,10 @@
 {
 {
     "name": "guzzlehttp/promises",
     "name": "guzzlehttp/promises",
     "description": "Guzzle promises library",
     "description": "Guzzle promises library",
-    "keywords": ["promise"],
     "license": "MIT",
     "license": "MIT",
+    "keywords": [
+        "promise"
+    ],
     "authors": [
     "authors": [
         {
         {
             "name": "Graham Campbell",
             "name": "Graham Campbell",
@@ -26,11 +28,12 @@
         }
         }
     ],
     ],
     "require": {
     "require": {
-        "php": "^7.2.5 || ^8.0"
+        "php": "^7.2.5 || ^8.0",
+        "symfony/deprecation-contracts": "^2.5 || ^3.0"
     },
     },
     "require-dev": {
     "require-dev": {
         "bamarni/composer-bin-plugin": "^1.8.2",
         "bamarni/composer-bin-plugin": "^1.8.2",
-        "phpunit/phpunit": "^8.5.39 || ^9.6.20"
+        "phpunit/phpunit": "^8.5.52 || ^9.6.34"
     },
     },
     "autoload": {
     "autoload": {
         "psr-4": {
         "psr-4": {
@@ -42,17 +45,17 @@
             "GuzzleHttp\\Promise\\Tests\\": "tests/"
             "GuzzleHttp\\Promise\\Tests\\": "tests/"
         }
         }
     },
     },
-    "extra": {
-        "bamarni-bin": {
-            "bin-links": true,
-            "forward-command": false
-        }
-    },
     "config": {
     "config": {
         "allow-plugins": {
         "allow-plugins": {
             "bamarni/composer-bin-plugin": true
             "bamarni/composer-bin-plugin": true
         },
         },
         "preferred-install": "dist",
         "preferred-install": "dist",
         "sort-packages": true
         "sort-packages": true
+    },
+    "extra": {
+        "bamarni-bin": {
+            "bin-links": true,
+            "forward-command": false
+        }
     }
     }
 }
 }

+ 4 - 1
modules/boonex/xero/plugins/guzzlehttp/promises/src/Coroutine.php

@@ -117,7 +117,10 @@ final class Coroutine implements PromiseInterface
 
 
     public function cancel(): void
     public function cancel(): void
     {
     {
-        $this->currentPromise->cancel();
+        if (isset($this->currentPromise)) {
+            $this->currentPromise->cancel();
+        }
+
         $this->result->cancel();
         $this->result->cancel();
     }
     }
 
 

+ 10 - 0
modules/boonex/xero/plugins/guzzlehttp/promises/src/Create.php

@@ -74,6 +74,16 @@ final class Create
             return new \ArrayIterator($value);
             return new \ArrayIterator($value);
         }
         }
 
 
+        if (!is_iterable($value)) {
+            \trigger_deprecation(
+                'guzzlehttp/promises',
+                '2.5',
+                'Passing a non-iterable to %s::%s() is deprecated; guzzlehttp/promises 3.0 will require an iterable.',
+                __CLASS__,
+                __FUNCTION__
+            );
+        }
+
         return new \ArrayIterator([$value]);
         return new \ArrayIterator([$value]);
     }
     }
 }
 }

+ 23 - 0
modules/boonex/xero/plugins/guzzlehttp/promises/src/Each.php

@@ -26,6 +26,8 @@ final class Each
         ?callable $onFulfilled = null,
         ?callable $onFulfilled = null,
         ?callable $onRejected = null
         ?callable $onRejected = null
     ): PromiseInterface {
     ): PromiseInterface {
+        $iterable = self::prepareIterable($iterable, __FUNCTION__);
+
         return (new EachPromise($iterable, [
         return (new EachPromise($iterable, [
             'fulfilled' => $onFulfilled,
             'fulfilled' => $onFulfilled,
             'rejected' => $onRejected,
             'rejected' => $onRejected,
@@ -49,6 +51,8 @@ final class Each
         ?callable $onFulfilled = null,
         ?callable $onFulfilled = null,
         ?callable $onRejected = null
         ?callable $onRejected = null
     ): PromiseInterface {
     ): PromiseInterface {
+        $iterable = self::prepareIterable($iterable, __FUNCTION__);
+
         return (new EachPromise($iterable, [
         return (new EachPromise($iterable, [
             'fulfilled' => $onFulfilled,
             'fulfilled' => $onFulfilled,
             'rejected' => $onRejected,
             'rejected' => $onRejected,
@@ -69,6 +73,8 @@ final class Each
         $concurrency,
         $concurrency,
         ?callable $onFulfilled = null
         ?callable $onFulfilled = null
     ): PromiseInterface {
     ): PromiseInterface {
+        $iterable = self::prepareIterable($iterable, __FUNCTION__);
+
         return self::ofLimit(
         return self::ofLimit(
             $iterable,
             $iterable,
             $concurrency,
             $concurrency,
@@ -78,4 +84,21 @@ final class Each
             }
             }
         );
         );
     }
     }
+
+    private static function prepareIterable($iterable, string $method): iterable
+    {
+        if (is_iterable($iterable)) {
+            return $iterable;
+        }
+
+        \trigger_deprecation(
+            'guzzlehttp/promises',
+            '2.5',
+            'Passing a non-iterable to %s::%s() is deprecated; guzzlehttp/promises 3.0 will require an iterable.',
+            self::class,
+            $method
+        );
+
+        return [$iterable];
+    }
 }
 }

+ 62 - 12
modules/boonex/xero/plugins/guzzlehttp/promises/src/EachPromise.php

@@ -34,6 +34,9 @@ class EachPromise implements PromisorInterface
     /** @var bool|null */
     /** @var bool|null */
     private $mutex;
     private $mutex;
 
 
+    /** @var bool */
+    private $stepWhileLocked = false;
+
     /**
     /**
      * Configuration hash can include the following key value pairs:
      * Configuration hash can include the following key value pairs:
      *
      *
@@ -57,6 +60,18 @@ class EachPromise implements PromisorInterface
      */
      */
     public function __construct($iterable, array $config = [])
     public function __construct($iterable, array $config = [])
     {
     {
+        if (!is_iterable($iterable)) {
+            \trigger_deprecation(
+                'guzzlehttp/promises',
+                '2.5',
+                'Passing a non-iterable to %s::%s() is deprecated; guzzlehttp/promises 3.0 will require an iterable.',
+                __CLASS__,
+                __FUNCTION__
+            );
+
+            $iterable = [$iterable];
+        }
+
         $this->iterable = Create::iterFor($iterable);
         $this->iterable = Create::iterFor($iterable);
 
 
         if (isset($config['concurrency'])) {
         if (isset($config['concurrency'])) {
@@ -84,6 +99,19 @@ class EachPromise implements PromisorInterface
             /** @psalm-assert Promise $this->aggregate */
             /** @psalm-assert Promise $this->aggregate */
             $this->iterable->rewind();
             $this->iterable->rewind();
             $this->refillPending();
             $this->refillPending();
+            if (!$this->pending) {
+                Utils::queue()->add(function (): void {
+                    if (!$this->aggregate || Is::settled($this->aggregate)) {
+                        return;
+                    }
+
+                    try {
+                        $this->checkIfFinished();
+                    } catch (\Throwable $e) {
+                        $this->aggregate->reject($e);
+                    }
+                });
+            }
         } catch (\Throwable $e) {
         } catch (\Throwable $e) {
             $this->aggregate->reject($e);
             $this->aggregate->reject($e);
         }
         }
@@ -98,16 +126,23 @@ class EachPromise implements PromisorInterface
     {
     {
         $this->mutex = false;
         $this->mutex = false;
         $this->aggregate = new Promise(function (): void {
         $this->aggregate = new Promise(function (): void {
-            if ($this->checkIfFinished()) {
-                return;
-            }
-            reset($this->pending);
-            // Consume a potentially fluctuating list of promises while
-            // ensuring that indexes are maintained (precluding array_shift).
-            while ($promise = current($this->pending)) {
-                next($this->pending);
-                $promise->wait();
-                if (Is::settled($this->aggregate)) {
+            while (true) {
+                if ($this->checkIfFinished()) {
+                    return;
+                }
+                reset($this->pending);
+                // Consume a potentially fluctuating list of promises while
+                // ensuring that indexes are maintained (precluding array_shift).
+                while ($promise = current($this->pending)) {
+                    next($this->pending);
+                    $promise->wait();
+                    if (Is::settled($this->aggregate)) {
+                        return;
+                    }
+                }
+                // Refill and re-sweep; give up only when nothing remains.
+                $this->refillPending();
+                if (Is::settled($this->aggregate) || !$this->pending) {
                     return;
                     return;
                 }
                 }
             }
             }
@@ -137,6 +172,10 @@ class EachPromise implements PromisorInterface
         $concurrency = is_callable($this->concurrency)
         $concurrency = is_callable($this->concurrency)
             ? ($this->concurrency)(count($this->pending))
             ? ($this->concurrency)(count($this->pending))
             : $this->concurrency;
             : $this->concurrency;
+        // The callable can settle the aggregate; admit nothing more.
+        if (Is::settled($this->aggregate)) {
+            return;
+        }
         $concurrency = max($concurrency - count($this->pending), 0);
         $concurrency = max($concurrency - count($this->pending), 0);
         // Concurrency may be set to 0 to disallow new promises.
         // Concurrency may be set to 0 to disallow new promises.
         if (!$concurrency) {
         if (!$concurrency) {
@@ -198,6 +237,8 @@ class EachPromise implements PromisorInterface
         // Place a lock on the iterator so that we ensure to not recurse,
         // Place a lock on the iterator so that we ensure to not recurse,
         // preventing fatal generator errors.
         // preventing fatal generator errors.
         if ($this->mutex) {
         if ($this->mutex) {
+            $this->stepWhileLocked = true;
+
             return false;
             return false;
         }
         }
 
 
@@ -206,14 +247,22 @@ class EachPromise implements PromisorInterface
         try {
         try {
             $this->iterable->next();
             $this->iterable->next();
             $this->mutex = false;
             $this->mutex = false;
-
-            return true;
         } catch (\Throwable $e) {
         } catch (\Throwable $e) {
             $this->aggregate->reject($e);
             $this->aggregate->reject($e);
             $this->mutex = false;
             $this->mutex = false;
 
 
             return false;
             return false;
         }
         }
+
+        // Run the completion check that locked steps skipped.
+        if ($this->stepWhileLocked) {
+            $this->stepWhileLocked = false;
+            if (!Is::settled($this->aggregate)) {
+                $this->checkIfFinished();
+            }
+        }
+
+        return true;
     }
     }
 
 
     private function step(int $idx): void
     private function step(int $idx): void
@@ -234,6 +283,7 @@ class EachPromise implements PromisorInterface
         }
         }
     }
     }
 
 
+    /** @phpstan-impure */
     private function checkIfFinished(): bool
     private function checkIfFinished(): bool
     {
     {
         if (!$this->pending && !$this->iterable->valid()) {
         if (!$this->pending && !$this->iterable->valid()) {

+ 52 - 2
modules/boonex/xero/plugins/guzzlehttp/promises/src/Utils.php

@@ -76,9 +76,15 @@ final class Utils
                 'state' => PromiseInterface::FULFILLED,
                 'state' => PromiseInterface::FULFILLED,
                 'value' => $promise->wait(),
                 'value' => $promise->wait(),
             ];
             ];
-        } catch (RejectionException $e) {
-            return ['state' => PromiseInterface::REJECTED, 'reason' => $e->getReason()];
         } catch (\Throwable $e) {
         } catch (\Throwable $e) {
+            if ($e instanceof AggregateException) {
+                return ['state' => PromiseInterface::REJECTED, 'reason' => $e];
+            }
+
+            if ($e instanceof RejectionException) {
+                return ['state' => PromiseInterface::REJECTED, 'reason' => $e->getReason()];
+            }
+
             return ['state' => PromiseInterface::REJECTED, 'reason' => $e];
             return ['state' => PromiseInterface::REJECTED, 'reason' => $e];
         }
         }
     }
     }
@@ -95,6 +101,8 @@ final class Utils
      */
      */
     public static function inspectAll($promises): array
     public static function inspectAll($promises): array
     {
     {
+        $promises = self::prepareIterable($promises, __FUNCTION__);
+
         $results = [];
         $results = [];
         foreach ($promises as $key => $promise) {
         foreach ($promises as $key => $promise) {
             $results[$key] = self::inspect($promise);
             $results[$key] = self::inspect($promise);
@@ -116,6 +124,8 @@ final class Utils
      */
      */
     public static function unwrap($promises): array
     public static function unwrap($promises): array
     {
     {
+        $promises = self::prepareIterable($promises, __FUNCTION__);
+
         $results = [];
         $results = [];
         foreach ($promises as $key => $promise) {
         foreach ($promises as $key => $promise) {
             $results[$key] = $promise->wait();
             $results[$key] = $promise->wait();
@@ -137,6 +147,8 @@ final class Utils
      */
      */
     public static function all($promises, bool $recursive = false): PromiseInterface
     public static function all($promises, bool $recursive = false): PromiseInterface
     {
     {
+        $promises = self::prepareIterable($promises, __FUNCTION__);
+
         $results = [];
         $results = [];
         $promise = Each::of(
         $promise = Each::of(
             $promises,
             $promises,
@@ -156,6 +168,12 @@ final class Utils
 
 
         if (true === $recursive) {
         if (true === $recursive) {
             $promise = $promise->then(function ($results) use ($recursive, &$promises) {
             $promise = $promise->then(function ($results) use ($recursive, &$promises) {
+                // A consumed generator cannot be traversed again, so a
+                // recursive pass has nothing further to observe.
+                if ($promises instanceof \Generator) {
+                    return $results;
+                }
+
                 foreach ($promises as $promise) {
                 foreach ($promises as $promise) {
                     if (Is::pending($promise)) {
                     if (Is::pending($promise)) {
                         return self::all($promises, $recursive);
                         return self::all($promises, $recursive);
@@ -185,6 +203,8 @@ final class Utils
      */
      */
     public static function some(int $count, $promises): PromiseInterface
     public static function some(int $count, $promises): PromiseInterface
     {
     {
+        $promises = self::prepareIterable($promises, __FUNCTION__);
+
         $results = [];
         $results = [];
         $rejections = [];
         $rejections = [];
 
 
@@ -225,6 +245,8 @@ final class Utils
      */
      */
     public static function any($promises): PromiseInterface
     public static function any($promises): PromiseInterface
     {
     {
+        $promises = self::prepareIterable($promises, __FUNCTION__);
+
         return self::some(1, $promises)->then(function ($values) {
         return self::some(1, $promises)->then(function ($values) {
             return $values[0];
             return $values[0];
         });
         });
@@ -242,6 +264,8 @@ final class Utils
      */
      */
     public static function settle($promises): PromiseInterface
     public static function settle($promises): PromiseInterface
     {
     {
+        $promises = self::prepareIterable($promises, __FUNCTION__);
+
         $results = [];
         $results = [];
 
 
         return Each::of(
         return Each::of(
@@ -258,4 +282,30 @@ final class Utils
             return $results;
             return $results;
         });
         });
     }
     }
+
+    private static function prepareIterable($promises, string $method): iterable
+    {
+        if (is_iterable($promises)) {
+            return $promises;
+        }
+
+        self::triggerNonIterableDeprecation($promises, $method);
+
+        return [$promises];
+    }
+
+    private static function triggerNonIterableDeprecation($promises, string $method): void
+    {
+        if (is_iterable($promises)) {
+            return;
+        }
+
+        \trigger_deprecation(
+            'guzzlehttp/promises',
+            '2.5',
+            'Passing a non-iterable to %s::%s() is deprecated; guzzlehttp/promises 3.0 will require an iterable.',
+            self::class,
+            $method
+        );
+    }
 }
 }

+ 179 - 0
modules/boonex/xero/plugins/guzzlehttp/psr7/CHANGELOG.md

@@ -5,6 +5,185 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 
+## 2.13.1 - 2026-08-24
+
+### Added
+
+- Add support for PHP 8.6
+
+### Fixed
+
+- Prefix relative paths that begin with a colon segment with `./` instead of throwing
+
+## 2.13.0 - 2026-07-16
+
+### Added
+
+- Add `Utils::` `asciiToLower`, `asciiToUpper`, `asciiUcFirst`, `caselessEquals`, `caselessContains`
+
+### Changed
+
+- Use locale-independent ASCII case folding everywhere case is normalized
+- Trigger a runtime deprecation for previously deprecated functionality in 2.3.0
+
+## 2.12.5 - 2026-07-13
+
+### Fixed
+
+- Compare header names and hosts with locale-independent ASCII lowercasing
+- Compare hosts without locale sensitivity when detecting cross-origin redirects
+
+## 2.12.4 - 2026-07-08
+
+### Changed
+
+- Pass explicit trim characters ahead of the PHP 8.6 trim default change
+
+### Fixed
+
+- Anchor server port and response start-line patterns to the true end of input
+- Treat host-less origin-form request targets starting with `//` as paths in `Message::parseRequest()`
+- Reject raw DEL bytes in bracketed IP-literal hosts instead of parsing a mutated host
+- Reject invalid bytes after a bracketed IP-literal host instead of reparsing a different host
+
+## 2.12.3 - 2026-06-23
+
+### Security
+
+- Validate the URI host so `getHost()` matches the URI authority (GHSA-c2w2-prh8-qm98)
+
+## 2.12.2 - 2026-06-23
+
+### Fixed
+
+- Report URI parsing, filtering, and normalization PCRE failures explicitly
+- Report HTTP message parser PCRE failures explicitly
+- Fail closed when PCRE validation fails for request targets and hosts
+
+## 2.12.1 - 2026-06-18
+
+### Security
+
+- Reject CR/LF in HTTP method, protocol version, and reason phrase (GHSA-vm85-hxw5-5432)
+
+## 2.12.0 - 2026-06-16
+
+### Deprecated
+
+- Deprecated non-finite float values in `Query::build()` that guzzlehttp/psr7 3.0 rejects
+- Deprecated non-finite float multipart contents that guzzlehttp/psr7 3.0 rejects
+- Deprecated non-string scalar bodies in `Utils::streamFor()`; cast them to a string for 3.0
+- Deprecated non-string `Uri::withQueryValues()` values; cast them to a string for 3.0
+
+## 2.11.1 - 2026-06-12
+
+### Fixed
+
+- Fixed non-finite float values emitting coercion warnings on PHP 8.5
+
+## 2.11.0 - 2026-06-02
+
+### Changed
+
+- Changed `Utils::modifyRequest()` to reject conflicting URI and `Host` header changes in the same call
+- Changed `Header::parse()` to split semicolon-separated parameters without repeated regular expression lookaheads
+- Changed `UriComparator::isCrossOrigin()` so only HTTP and HTTPS missing ports receive implicit default ports
+
+### Deprecated
+
+- Deprecated invalid PSR-7 arguments that guzzlehttp/psr7 3.0 will require native types for
+- Deprecated non-string header values that guzzlehttp/psr7 3.0 will reject
+- Deprecated empty header value arrays that guzzlehttp/psr7 3.0 will reject
+- Deprecated URI schemes that do not match guzzlehttp/psr7 3.0 syntax requirements
+- Deprecated multipart boundary and custom part header metadata that guzzlehttp/psr7 3.0 will reject
+- Deprecated reliance on automatic uppercasing of request methods; guzzlehttp/psr7 3.0 preserves method casing
+- Deprecated invalid `Utils::modifyRequest()` change values that guzzlehttp/psr7 3.0 will reject
+
+### Fixed
+
+- Fixed `Utils::copyToStream()` to retry short destination writes instead of dropping the unwritten remainder
+- Fixed `Header::parse()` splitting of semicolon-separated parameters with escaped quotes
+
+## 2.10.4 - 2026-05-29
+
+### Fixed
+
+- Apply `UriNormalizer` percent-encoding normalizations to URI fragments
+- Make `LimitStream::getSize()` return `0` for slices past the underlying stream end
+- Make `AppendStream::read()` return an empty string when no streams are attached
+- Make `CachingStream::read()` throw on an incomplete cache-target write instead of silently corrupting replays
+- Prevent `CachingStream::seek()` from looping indefinitely when the remote stream makes no progress
+
+## 2.10.3 - 2026-05-27
+
+### Fixed
+
+- Fixed URI parsing for IPv6 literals containing embedded IPv4 addresses
+- Fixed malformed UTF-8 URI strings being parsed as empty URIs
+
+## 2.10.2 - 2026-05-25
+
+### Security
+
+- Reject control and whitespace characters in URI host components (GHSA-hq7v-mx3g-29hw)
+- Reject malformed Host values when constructing request URIs (GHSA-34xg-wgjx-8xph)
+
+### Fixed
+
+- Make `ServerRequest::fromGlobals()` robust against unexpected HTTP header value types in `$_SERVER`
+
+## 2.10.1 - 2026-05-20
+
+### Fixed
+
+- Fix `Utils::modifyRequest()` with numeric header names
+
+## 2.10.0 - 2026-05-19
+
+### Changed
+
+- Harden `ServerRequest::fromGlobals()` against malformed `$_SERVER` values
+- Prevent custom stream metadata from affecting internal size handling
+- Throw when `StreamWrapper::getResource()` cannot create a resource
+- Preserve custom request implementations in `Utils::modifyRequest()`
+- Preserve custom URI implementations in `UriResolver::resolve()`
+- Make `Uri::__toString()` side-effect-free
+
+## 2.9.1 - 2026-05-19
+
+### Fixed
+
+- Fix parsing of relative path references containing a colon in a non-initial path segment
+- Fix `CachingStream::detach()` returning an incomplete resource before the decorated stream has been fully read
+- Fix `Message::bodySummary()` returning `null` when truncating printable UTF-8 bodies inside a multibyte character
+
+## 2.9.0 - 2026-03-10
+
+### Added
+
+- Added nested array expansion support to `MultipartStream`
+- Added `@return static` to `MessageTrait` methods
+
+### Changed
+
+- Updated MIME type mappings
+
+## 2.8.1 - 2026-03-10
+
+### Fixed
+
+- Encode `+` signs in `Uri::withQueryValue()` and `Uri::withQueryValues()` to prevent them being interpreted as spaces
+
+## 2.8.0 - 2025-08-23
+
+### Added
+
+- Allow empty lists as header values
+
+### Changed
+
+- PHP 8.5 support
+
 ## 2.7.1 - 2025-03-27
 ## 2.7.1 - 2025-03-27
 
 
 ### Fixed
 ### Fixed

+ 98 - 44
modules/boonex/xero/plugins/guzzlehttp/psr7/README.md

@@ -25,7 +25,9 @@ composer require guzzlehttp/psr7
 | Version | Status              | PHP Version  |
 | Version | Status              | PHP Version  |
 |---------|---------------------|--------------|
 |---------|---------------------|--------------|
 | 1.x     | EOL (2024-06-30)    | >=5.4,<8.2   |
 | 1.x     | EOL (2024-06-30)    | >=5.4,<8.2   |
-| 2.x     | Latest              | >=7.2.5,<8.5 |
+| 2.x     | Latest              | >=7.2.5,<8.7 |
+
+See [UPGRADING.md](UPGRADING.md) for notes on upgrading from 1.x to 2.0.
 
 
 
 
 ## AppendStream
 ## AppendStream
@@ -62,7 +64,7 @@ preferred size of the buffer.
 use GuzzleHttp\Psr7;
 use GuzzleHttp\Psr7;
 
 
 // When more than 1024 bytes are in the buffer, it will begin returning
 // When more than 1024 bytes are in the buffer, it will begin returning
-// false to writes. This is an indication that writers should slow down.
+// 0 to writes. This is an indication that writers should slow down.
 $buffer = new Psr7\BufferStream(1024);
 $buffer = new Psr7\BufferStream(1024);
 ```
 ```
 
 
@@ -117,7 +119,7 @@ echo $stream; // 0123456789
 
 
 `GuzzleHttp\Psr7\FnStream`
 `GuzzleHttp\Psr7\FnStream`
 
 
-Compose stream implementations based on a hash of functions.
+Compose stream implementations based on a hash of callables.
 
 
 Allows for easy testing and extension of a provided stream without needing
 Allows for easy testing and extension of a provided stream without needing
 to create a concrete class for a simple extension point.
 to create a concrete class for a simple extension point.
@@ -200,6 +202,11 @@ echo $stream->tell();
 Stream that when read returns bytes for a streaming multipart or
 Stream that when read returns bytes for a streaming multipart or
 multipart/form-data stream.
 multipart/form-data stream.
 
 
+Each multipart element must contain a `name` and `contents` key. `contents` may
+be any non-array value accepted by `GuzzleHttp\Psr7\Utils::streamFor()`,
+including closures and invokable objects. Array contents are recursively
+expanded into nested form fields.
+
 
 
 ## NoSeekStream
 ## NoSeekStream
 
 
@@ -229,12 +236,15 @@ var_export($noSeek->read(3));
 
 
 Provides a read only stream that pumps data from a PHP callable.
 Provides a read only stream that pumps data from a PHP callable.
 
 
-When invoking the provided callable, the PumpStream will pass the amount of
-data requested to read to the callable. The callable can choose to ignore
+When invoking the provided callable, the PumpStream will pass the suggested
+number of bytes to read to the callable. The callable can choose to ignore
 this value and return fewer or more bytes than requested. Any extra data
 this value and return fewer or more bytes than requested. Any extra data
 returned by the provided callable is buffered internally until drained using
 returned by the provided callable is buffered internally until drained using
 the read() function of the PumpStream. The provided callable MUST return
 the read() function of the PumpStream. The provided callable MUST return
-false when there is no more data to read.
+false or null when there is no more data to read.
+
+Userland callables that declare no parameters are tolerated by PHP, but
+length-aware callables remain the recommended formal shape.
 
 
 
 
 ## Implementing stream decorators
 ## Implementing stream decorators
@@ -445,6 +455,56 @@ string. This function does not modify the provided keys when an array is
 encountered (like `http_build_query()` would).
 encountered (like `http_build_query()` would).
 
 
 
 
+## `GuzzleHttp\Psr7\Utils::asciiToLower`
+
+`public static function asciiToLower(string $string): string`
+
+Converts ASCII uppercase letters in a string to lowercase.
+
+Unlike strtolower(), which honors LC_CTYPE before PHP 8.2, the conversion is
+locale-independent and leaves every non-ASCII byte unchanged, as HTTP protocol
+elements require.
+
+
+## `GuzzleHttp\Psr7\Utils::asciiToUpper`
+
+`public static function asciiToUpper(string $string): string`
+
+Converts ASCII lowercase letters in a string to uppercase.
+
+Unlike strtoupper(), which honors LC_CTYPE before PHP 8.2, the conversion is
+locale-independent and leaves every non-ASCII byte unchanged, as HTTP protocol
+elements require.
+
+
+## `GuzzleHttp\Psr7\Utils::asciiUcFirst`
+
+`public static function asciiUcFirst(string $string): string`
+
+Converts the first character of a string to uppercase when it is an ASCII
+lowercase letter.
+
+Unlike ucfirst(), which honors LC_CTYPE before PHP 8.2, the conversion is
+locale-independent and leaves every non-ASCII byte unchanged, as HTTP protocol
+elements require.
+
+
+## `GuzzleHttp\Psr7\Utils::caselessContains`
+
+`public static function caselessContains(string $haystack, string $needle): bool`
+
+Checks whether the haystack contains the needle, comparing ASCII letters
+case-insensitively and without locale sensitivity.
+
+
+## `GuzzleHttp\Psr7\Utils::caselessEquals`
+
+`public static function caselessEquals(string $left, string $right): bool`
+
+Checks whether two strings are equal, comparing ASCII letters
+case-insensitively and without locale sensitivity.
+
+
 ## `GuzzleHttp\Psr7\Utils::caselessRemove`
 ## `GuzzleHttp\Psr7\Utils::caselessRemove`
 
 
 `public static function caselessRemove(iterable<string> $keys, $keys, array $data): array`
 `public static function caselessRemove(iterable<string> $keys, $keys, array $data): array`
@@ -459,6 +519,11 @@ Remove the items given by the keys, case insensitively from the data.
 Copy the contents of a stream into another stream until the given number
 Copy the contents of a stream into another stream until the given number
 of bytes have been read.
 of bytes have been read.
 
 
+The copy stops if the destination `write()` returns 0, for example a
+`BufferStream` at its high water mark or a full `DroppingStream`. For a
+guaranteed full copy, use a normal writable stream such as a file or
+`php://temp` stream.
+
 
 
 ## `GuzzleHttp\Psr7\Utils::copyToString`
 ## `GuzzleHttp\Psr7\Utils::copyToString`
 
 
@@ -490,7 +555,10 @@ a message.
 - method: (string) Changes the HTTP method.
 - method: (string) Changes the HTTP method.
 - set_headers: (array) Sets the given headers.
 - set_headers: (array) Sets the given headers.
 - remove_headers: (array) Remove the given headers.
 - remove_headers: (array) Remove the given headers.
-- body: (mixed) Sets the given body.
+- body: (mixed) Sets the given body. Present non-null values are converted with
+  `GuzzleHttp\Psr7\Utils::streamFor()`, including scalar values, resources,
+  streams, iterators, callable arrays, closures, invokable objects, and
+  objects with `__toString()`. String inputs remain literal bodies.
 - uri: (UriInterface) Set the URI.
 - uri: (UriInterface) Set the URI.
 - query: (string) Set the query string value of the URI.
 - query: (string) Set the query string value of the URI.
 - version: (string) Set the protocol version.
 - version: (string) Set the protocol version.
@@ -536,13 +604,14 @@ This method accepts the following `$resource` types:
   the object will be cast to a string and then a stream will be returned that
   the object will be cast to a string and then a stream will be returned that
   uses the string value.
   uses the string value.
 - `NULL`: When `null` is passed, an empty stream object is returned.
 - `NULL`: When `null` is passed, an empty stream object is returned.
-- `callable` When a callable is passed, a read-only stream object will be
+- `callable`: When a callable array, closure, or invokable object is passed and
+  no earlier resource or object rule applies, a read-only stream object will be
   created that invokes the given callable. The callable is invoked with the
   created that invokes the given callable. The callable is invoked with the
-  number of suggested bytes to read. The callable can return any number of
-  bytes, but MUST return `false` when there is no more data to return. The
-  stream object that wraps the callable will invoke the callable until the
-  number of requested bytes are available. Any additional bytes will be
-  buffered and used in subsequent reads.
+  suggested number of bytes to read. The callable can return fewer or more bytes
+  than requested, but MUST return `false` or `null` when there is no more data
+  to return. Any additional bytes will be buffered and used in subsequent reads.
+  String inputs are always treated as string bodies, even when they name
+  callable functions.
 
 
 ```php
 ```php
 $stream = GuzzleHttp\Psr7\Utils::streamFor('foo');
 $stream = GuzzleHttp\Psr7\Utils::streamFor('foo');
@@ -604,36 +673,6 @@ Determines the mimetype of a file by looking at its extension.
 Maps a file extensions to a mimetype.
 Maps a file extensions to a mimetype.
 
 
 
 
-## Upgrading from Function API
-
-The static API was first introduced in 1.7.0, in order to mitigate problems with functions conflicting between global and local copies of the package. The function API was removed in 2.0.0. A migration table has been provided here for your convenience:
-
-| Original Function | Replacement Method |
-|----------------|----------------|
-| `str` | `Message::toString` |
-| `uri_for` | `Utils::uriFor` |
-| `stream_for` | `Utils::streamFor` |
-| `parse_header` | `Header::parse` |
-| `normalize_header` | `Header::normalize` |
-| `modify_request` | `Utils::modifyRequest` |
-| `rewind_body` | `Message::rewindBody` |
-| `try_fopen` | `Utils::tryFopen` |
-| `copy_to_string` | `Utils::copyToString` |
-| `copy_to_stream` | `Utils::copyToStream` |
-| `hash` | `Utils::hash` |
-| `readline` | `Utils::readLine` |
-| `parse_request` | `Message::parseRequest` |
-| `parse_response` | `Message::parseResponse` |
-| `parse_query` | `Query::parse` |
-| `build_query` | `Query::build` |
-| `mimetype_from_filename` | `MimeType::fromFilename` |
-| `mimetype_from_extension` | `MimeType::fromExtension` |
-| `_parse_message` | `Message::parseMessage` |
-| `_parse_request_uri` | `Message::parseRequestUri` |
-| `get_message_body_summary` | `Message::bodySummary` |
-| `_caseless_remove` | `Utils::caselessRemove` |
-
-
 # Additional URI Methods
 # Additional URI Methods
 
 
 Aside from the standard `Psr\Http\Message\UriInterface` implementation in form of the `GuzzleHttp\Psr7\Uri` class,
 Aside from the standard `Psr\Http\Message\UriInterface` implementation in form of the `GuzzleHttp\Psr7\Uri` class,
@@ -745,6 +784,10 @@ provided key are removed.
 
 
 Determines if a modified URL should be considered cross-origin with respect to an original URL.
 Determines if a modified URL should be considered cross-origin with respect to an original URL.
 
 
+Two URLs are cross-origin when their scheme, host, or effective port differ. Host comparison is case-insensitive, and missing ports use the default port for `http` or `https`. Other schemes do not receive implicit default ports.
+
+This helper only compares URI origins. It does not implement redirect handling or credential policy.
+
 ## Reference Resolution
 ## Reference Resolution
 
 
 `GuzzleHttp\Psr7\UriResolver` provides methods to resolve a URI reference in the context of a base URI according
 `GuzzleHttp\Psr7\UriResolver` provides methods to resolve a URI reference in the context of a base URI according
@@ -757,6 +800,10 @@ browsers do when resolving a link in a website based on the current request URI.
 
 
 Converts the relative URI into a new URI that is resolved against the base URI.
 Converts the relative URI into a new URI that is resolved against the base URI.
 
 
+When the resolved path is a relative-path reference whose first segment contains a colon, which would be mistaken for a
+scheme name ([RFC 3986 Section 4.2](https://datatracker.ietf.org/doc/html/rfc3986#section-4.2)), it is prefixed with
+`./`, e.g. `./a:b`.
+
 ### `GuzzleHttp\Psr7\UriResolver::removeDotSegments`
 ### `GuzzleHttp\Psr7\UriResolver::removeDotSegments`
 
 
 `public static function removeDotSegments(string $path): string`
 `public static function removeDotSegments(string $path): string`
@@ -796,7 +843,14 @@ echo UriResolver::relativize($base, new Uri('http://example.org/a/b/'));   // pr
 
 
 Returns a normalized URI. The scheme and host component are already normalized to lowercase per PSR-7 UriInterface.
 Returns a normalized URI. The scheme and host component are already normalized to lowercase per PSR-7 UriInterface.
 This methods adds additional normalizations that can be configured with the `$flags` parameter which is a bitmask
 This methods adds additional normalizations that can be configured with the `$flags` parameter which is a bitmask
-of normalizations to apply. The following normalizations are available:
+of normalizations to apply.
+
+When a normalization rewrites the path of a relative-path reference so that its first segment contains a colon, which
+would be mistaken for a scheme name ([RFC 3986 Section 4.2](https://datatracker.ietf.org/doc/html/rfc3986#section-4.2)),
+the path is prefixed with `./` instead of throwing, e.g. `a%41:` becomes `./aA:` as `aA:` would be an absolute URI
+with the scheme `aa`.
+
+The following normalizations are available:
 
 
 - `UriNormalizer::PRESERVING_NORMALIZATIONS`
 - `UriNormalizer::PRESERVING_NORMALIZATIONS`
 
 

+ 198 - 0
modules/boonex/xero/plugins/guzzlehttp/psr7/UPGRADING.md

@@ -0,0 +1,198 @@
+Guzzle PSR-7 Upgrade Guide
+==========================
+
+1.x to 2.0
+----------
+
+Guzzle PSR-7 2.0 is a major release that removes deprecated APIs, raises the
+minimum PHP version, and adds PHP 7 parameter and return types. Applications that
+only depend on PSR-7 interfaces should usually need small changes. Applications
+that call helper functions, extend package classes, or pass invalid argument
+types need closer review.
+
+#### PHP Version and Dependencies
+
+Guzzle PSR-7 2.0 requires PHP `^7.2.5 || ^8.0`. Guzzle PSR-7 1.x supported PHP
+`>=5.4.0`.
+
+Composer dependency changes that can affect upgrades:
+
+- `ralouphie/getallheaders` v2 support was dropped; 2.0 requires `^3.0`.
+- `psr/http-factory:^1.0` is required because 2.0 ships PSR-17 factories through `GuzzleHttp\Psr7\HttpFactory`.
+
+#### PHP 7 Type Hints and Return Types
+
+Type hints and return types were added wherever possible. Please make sure:
+
+- You pass values of the documented type when calling methods and functions.
+- Classes that extend Guzzle PSR-7 classes update any overridden method signatures to remain compatible.
+- Code that expected package-specific `InvalidArgumentException` exceptions for invalid argument types may now receive PHP `TypeError` exceptions instead.
+
+Common examples include passing a real integer status code to `Response::__construct()` and passing a string method to `Request::__construct()`.
+
+#### Removed Function API
+
+The static API was introduced in 1.7.0 to mitigate problems with functions
+conflicting between global and local copies of the package. The function API was
+removed in 2.0.0, along with the Composer `files` autoload entry that loaded
+`src/functions_include.php`.
+
+Replace namespaced function calls with the corresponding static methods in the
+`GuzzleHttp\Psr7` namespace:
+
+```php
+// Before:
+use function GuzzleHttp\Psr7\stream_for;
+
+$stream = stream_for('body');
+
+// After:
+use GuzzleHttp\Psr7\Utils;
+
+$stream = Utils::streamFor('body');
+```
+
+| Original Function | Replacement Method |
+|-------------------|--------------------|
+| `str` | `Message::toString` |
+| `uri_for` | `Utils::uriFor` |
+| `stream_for` | `Utils::streamFor` |
+| `parse_header` | `Header::parse` |
+| `normalize_header` | `Header::normalize` |
+| `modify_request` | `Utils::modifyRequest` |
+| `rewind_body` | `Message::rewindBody` |
+| `try_fopen` | `Utils::tryFopen` |
+| `copy_to_string` | `Utils::copyToString` |
+| `copy_to_stream` | `Utils::copyToStream` |
+| `hash` | `Utils::hash` |
+| `readline` | `Utils::readLine` |
+| `parse_request` | `Message::parseRequest` |
+| `parse_response` | `Message::parseResponse` |
+| `parse_query` | `Query::parse` |
+| `build_query` | `Query::build` |
+| `mimetype_from_filename` | `MimeType::fromFilename` |
+| `mimetype_from_extension` | `MimeType::fromExtension` |
+| `_parse_message` | `Message::parseMessage` |
+| `_parse_request_uri` | `Message::parseRequestUri` |
+| `get_message_body_summary` | `Message::bodySummary` |
+| `_caseless_remove` | `Utils::caselessRemove` |
+
+`Header::normalize()` remains the direct 2.0 replacement for
+`normalize_header()`. In newer 2.x versions, prefer `Header::splitList()` for
+new code.
+
+#### Deprecated URI Methods Removed
+
+The deprecated `Uri::resolve()` and `Uri::removeDotSegments()` methods were
+removed. Use `UriResolver` instead.
+
+```php
+// Before:
+$resolved = Uri::resolve($base, '../path');
+$path = Uri::removeDotSegments('/a/../b');
+
+// After:
+use GuzzleHttp\Psr7\UriResolver;
+use GuzzleHttp\Psr7\Utils;
+
+$resolved = UriResolver::resolve($base, Utils::uriFor('../path'));
+$path = UriResolver::removeDotSegments('/a/../b');
+```
+
+#### Stricter URI Validation
+
+Guzzle PSR-7 1.x automatically fixed a URI that combined an authority with a
+relative path by prepending `/` to the path. That deprecated behavior was removed
+in 2.0. Such URIs now throw `InvalidArgumentException`.
+
+```php
+// Before: automatically converted to //example.com/foo.
+$uri = (new Uri())->withHost('example.com')->withPath('foo');
+
+// After: make the absolute path explicit.
+$uri = (new Uri())->withHost('example.com')->withPath('/foo');
+```
+
+#### Header Validation
+
+Header names are validated more strictly according to RFC 7230 token syntax.
+Names containing whitespace, `/`, `(`, `)`, `\\`, or other invalid characters are
+rejected.
+
+If you construct messages from untrusted or non-standard input, normalize or
+reject invalid header names before constructing `Request`, `Response`, or
+`ServerRequest` instances.
+
+#### Query String Boolean Serialization
+
+`Query::build()` now serializes booleans as `1` and `0`, matching
+`http_build_query()` behavior.
+
+```php
+Query::build(['enabled' => true, 'disabled' => false]);
+// enabled=1&disabled=0
+```
+
+In current 2.x versions, pass `false` as the third argument if you need textual
+boolean values:
+
+```php
+Query::build(['enabled' => true, 'disabled' => false], PHP_QUERY_RFC3986, false);
+// enabled=true&disabled=false
+```
+
+#### Final Stream and Decorator Classes
+
+Several classes that were annotated with `@final` in 1.x are declared `final` in
+2.0:
+
+- `AppendStream`
+- `BufferStream`
+- `CachingStream`
+- `DroppingStream`
+- `FnStream`
+- `InflateStream`
+- `LazyOpenStream`
+- `LimitStream`
+- `MultipartStream`
+- `NoSeekStream`
+- `PumpStream`
+- `StreamWrapper`
+
+If your code extends one of these classes, replace inheritance with composition.
+For custom streams, implement `Psr\Http\Message\StreamInterface` directly or use
+`GuzzleHttp\Psr7\StreamDecoratorTrait` in your own class.
+
+`Request`, `Response`, `ServerRequest`, `Stream`, `UploadedFile`, and `Uri` remain
+extendable in 2.0, but overridden methods must have compatible signatures.
+
+#### Public Constants and Internal Details
+
+Some constants that were public in 1.x are implementation details in 2.0:
+
+- `Stream::READABLE_MODES`
+- `Stream::WRITABLE_MODES`
+- `Uri::HTTP_DEFAULT_HOST`
+
+If your code used these constants, define application-specific constants instead
+of depending on package internals.
+
+#### Stream Behavior Changes
+
+`BufferStream::write()` returns `0` instead of `false` when the buffer exceeds
+its high-water mark. This keeps the method compatible with the `int` return type
+from `StreamInterface::write()`.
+
+Several stream `__toString()` implementations now catch `Throwable`. On PHP 7.4
+and newer, exceptions thrown during stringification are rethrown. Avoid relying
+on `(string) $stream` to hide read failures; call `getContents()` or `read()` and
+handle exceptions when failures are possible.
+
+#### PSR-17 Factories
+
+Guzzle PSR-7 2.0 adds `GuzzleHttp\Psr7\HttpFactory`, an implementation of the
+PSR-17 factory interfaces from `psr/http-factory`. This is additive, but it is
+the reason for the new required dependency.
+
+For the full 2.0 diff, see
+https://github.com/guzzle/psr7/compare/1.8.1...2.0.0.

+ 29 - 13
modules/boonex/xero/plugins/guzzlehttp/psr7/composer.json

@@ -1,6 +1,7 @@
 {
 {
     "name": "guzzlehttp/psr7",
     "name": "guzzlehttp/psr7",
     "description": "PSR-7 message implementation that also provides common utility methods",
     "description": "PSR-7 message implementation that also provides common utility methods",
+    "license": "MIT",
     "keywords": [
     "keywords": [
         "request",
         "request",
         "response",
         "response",
@@ -11,7 +12,6 @@
         "url",
         "url",
         "psr-7"
         "psr-7"
     ],
     ],
-    "license": "MIT",
     "authors": [
     "authors": [
         {
         {
             "name": "Graham Campbell",
             "name": "Graham Campbell",
@@ -53,20 +53,36 @@
         "php": "^7.2.5 || ^8.0",
         "php": "^7.2.5 || ^8.0",
         "psr/http-factory": "^1.0",
         "psr/http-factory": "^1.0",
         "psr/http-message": "^1.1 || ^2.0",
         "psr/http-message": "^1.1 || ^2.0",
-        "ralouphie/getallheaders": "^3.0"
+        "ralouphie/getallheaders": "^3.0",
+        "symfony/deprecation-contracts": "^2.5 || ^3.0",
+        "symfony/polyfill-php80": "^1.25"
+    },
+    "require-dev": {
+        "bamarni/composer-bin-plugin": "^1.8.2",
+        "http-interop/http-factory-tests": "1.1.0",
+        "jshttp/mime-db": "1.54.0.1",
+        "phpunit/phpunit": "^8.5.52 || ^9.6.34"
     },
     },
     "provide": {
     "provide": {
         "psr/http-factory-implementation": "1.0",
         "psr/http-factory-implementation": "1.0",
         "psr/http-message-implementation": "1.0"
         "psr/http-message-implementation": "1.0"
     },
     },
-    "require-dev": {
-        "bamarni/composer-bin-plugin": "^1.8.2",
-        "http-interop/http-factory-tests": "0.9.0",
-        "phpunit/phpunit": "^8.5.39 || ^9.6.20"
-    },
     "suggest": {
     "suggest": {
         "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
         "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
     },
     },
+    "repositories": [
+        {
+            "type": "package",
+            "package": {
+                "name": "jshttp/mime-db",
+                "version": "1.54.0.1",
+                "dist": {
+                    "type": "zip",
+                    "url": "https://codeload.github.com/jshttp/mime-db/zip/0a9fd0bfbc87a725ff638495839114e7807b7177"
+                }
+            }
+        }
+    ],
     "autoload": {
     "autoload": {
         "psr-4": {
         "psr-4": {
             "GuzzleHttp\\Psr7\\": "src/"
             "GuzzleHttp\\Psr7\\": "src/"
@@ -77,17 +93,17 @@
             "GuzzleHttp\\Tests\\Psr7\\": "tests/"
             "GuzzleHttp\\Tests\\Psr7\\": "tests/"
         }
         }
     },
     },
-    "extra": {
-        "bamarni-bin": {
-            "bin-links": true,
-            "forward-command": false
-        }
-    },
     "config": {
     "config": {
         "allow-plugins": {
         "allow-plugins": {
             "bamarni/composer-bin-plugin": true
             "bamarni/composer-bin-plugin": true
         },
         },
         "preferred-install": "dist",
         "preferred-install": "dist",
         "sort-packages": true
         "sort-packages": true
+    },
+    "extra": {
+        "bamarni-bin": {
+            "bin-links": true,
+            "forward-command": false
+        }
     }
     }
 }
 }

+ 49 - 0
modules/boonex/xero/plugins/guzzlehttp/psr7/src/AppendStream.php

@@ -155,6 +155,24 @@ final class AppendStream implements StreamInterface
      */
      */
     public function seek($offset, $whence = SEEK_SET): void
     public function seek($offset, $whence = SEEK_SET): void
     {
     {
+        if (!\is_int($offset)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $offset.',
+                \get_debug_type($offset)
+            );
+        }
+
+        if (!\is_int($whence)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $whence.',
+                \get_debug_type($whence)
+            );
+        }
+
         if (!$this->seekable) {
         if (!$this->seekable) {
             throw new \RuntimeException('This AppendStream is not seekable');
             throw new \RuntimeException('This AppendStream is not seekable');
         } elseif ($whence !== SEEK_SET) {
         } elseif ($whence !== SEEK_SET) {
@@ -187,6 +205,19 @@ final class AppendStream implements StreamInterface
      */
      */
     public function read($length): string
     public function read($length): string
     {
     {
+        if (!\is_int($length)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing %s to StreamInterface::read() is deprecated; guzzlehttp/psr7 3.0 requires int for $length.',
+                \get_debug_type($length)
+            );
+        }
+
+        if ($this->streams === []) {
+            return '';
+        }
+
         $buffer = '';
         $buffer = '';
         $total = count($this->streams) - 1;
         $total = count($this->streams) - 1;
         $remaining = $length;
         $remaining = $length;
@@ -235,6 +266,15 @@ final class AppendStream implements StreamInterface
 
 
     public function write($string): int
     public function write($string): int
     {
     {
+        if (!\is_string($string)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing %s to StreamInterface::write() is deprecated; guzzlehttp/psr7 3.0 requires string for $string.',
+                \get_debug_type($string)
+            );
+        }
+
         throw new \RuntimeException('Cannot write to an AppendStream');
         throw new \RuntimeException('Cannot write to an AppendStream');
     }
     }
 
 
@@ -243,6 +283,15 @@ final class AppendStream implements StreamInterface
      */
      */
     public function getMetadata($key = null)
     public function getMetadata($key = null)
     {
     {
+        if ($key !== null && !\is_string($key)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing %s to StreamInterface::getMetadata() is deprecated; guzzlehttp/psr7 3.0 requires string|null for $key.',
+                \get_debug_type($key)
+            );
+        }
+
         return $key ? null : [];
         return $key ? null : [];
     }
     }
 }
 }

+ 45 - 0
modules/boonex/xero/plugins/guzzlehttp/psr7/src/BufferStream.php

@@ -86,6 +86,24 @@ final class BufferStream implements StreamInterface
 
 
     public function seek($offset, $whence = SEEK_SET): void
     public function seek($offset, $whence = SEEK_SET): void
     {
     {
+        if (!\is_int($offset)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $offset.',
+                \get_debug_type($offset)
+            );
+        }
+
+        if (!\is_int($whence)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $whence.',
+                \get_debug_type($whence)
+            );
+        }
+
         throw new \RuntimeException('Cannot seek a BufferStream');
         throw new \RuntimeException('Cannot seek a BufferStream');
     }
     }
 
 
@@ -104,6 +122,15 @@ final class BufferStream implements StreamInterface
      */
      */
     public function read($length): string
     public function read($length): string
     {
     {
+        if (!\is_int($length)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing %s to StreamInterface::read() is deprecated; guzzlehttp/psr7 3.0 requires int for $length.',
+                \get_debug_type($length)
+            );
+        }
+
         $currentLength = strlen($this->buffer);
         $currentLength = strlen($this->buffer);
 
 
         if ($length >= $currentLength) {
         if ($length >= $currentLength) {
@@ -124,6 +151,15 @@ final class BufferStream implements StreamInterface
      */
      */
     public function write($string): int
     public function write($string): int
     {
     {
+        if (!\is_string($string)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing %s to StreamInterface::write() is deprecated; guzzlehttp/psr7 3.0 requires string for $string.',
+                \get_debug_type($string)
+            );
+        }
+
         $this->buffer .= $string;
         $this->buffer .= $string;
 
 
         if (strlen($this->buffer) >= $this->hwm) {
         if (strlen($this->buffer) >= $this->hwm) {
@@ -138,6 +174,15 @@ final class BufferStream implements StreamInterface
      */
      */
     public function getMetadata($key = null)
     public function getMetadata($key = null)
     {
     {
+        if ($key !== null && !\is_string($key)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing %s to StreamInterface::getMetadata() is deprecated; guzzlehttp/psr7 3.0 requires string|null for $key.',
+                \get_debug_type($key)
+            );
+        }
+
         if ($key === 'hwm') {
         if ($key === 'hwm') {
             return $this->hwm;
             return $this->hwm;
         }
         }

+ 76 - 3
modules/boonex/xero/plugins/guzzlehttp/psr7/src/CachingStream.php

@@ -25,6 +25,9 @@ final class CachingStream implements StreamInterface
      */
      */
     private $stream;
     private $stream;
 
 
+    /** @var bool */
+    private $detached = false;
+
     /**
     /**
      * We will treat the buffer object as the body of the stream
      * We will treat the buffer object as the body of the stream
      *
      *
@@ -41,6 +44,10 @@ final class CachingStream implements StreamInterface
 
 
     public function getSize(): ?int
     public function getSize(): ?int
     {
     {
+        if ($this->detached) {
+            return null;
+        }
+
         $remoteSize = $this->remoteStream->getSize();
         $remoteSize = $this->remoteStream->getSize();
 
 
         if (null === $remoteSize) {
         if (null === $remoteSize) {
@@ -57,6 +64,24 @@ final class CachingStream implements StreamInterface
 
 
     public function seek($offset, $whence = SEEK_SET): void
     public function seek($offset, $whence = SEEK_SET): void
     {
     {
+        if (!\is_int($offset)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $offset.',
+                \get_debug_type($offset)
+            );
+        }
+
+        if (!\is_int($whence)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $whence.',
+                \get_debug_type($whence)
+            );
+        }
+
         if ($whence === SEEK_SET) {
         if ($whence === SEEK_SET) {
             $byte = $offset;
             $byte = $offset;
         } elseif ($whence === SEEK_CUR) {
         } elseif ($whence === SEEK_CUR) {
@@ -77,8 +102,16 @@ final class CachingStream implements StreamInterface
             // Read the remoteStream until we have read in at least the amount
             // Read the remoteStream until we have read in at least the amount
             // of bytes requested, or we reach the end of the file.
             // of bytes requested, or we reach the end of the file.
             while ($diff > 0 && !$this->remoteStream->eof()) {
             while ($diff > 0 && !$this->remoteStream->eof()) {
-                $this->read($diff);
-                $diff = $byte - $this->stream->getSize();
+                $previousSize = $this->stream->getSize();
+                $previousSkipReadBytes = $this->skipReadBytes;
+                $data = $this->read($diff);
+                $currentSize = $this->stream->getSize();
+
+                if ($data === '' && $currentSize === $previousSize && $this->skipReadBytes === $previousSkipReadBytes) {
+                    break;
+                }
+
+                $diff = $byte - $currentSize;
             }
             }
         } else {
         } else {
             // We can just do a normal seek since we've already seen this byte.
             // We can just do a normal seek since we've already seen this byte.
@@ -88,6 +121,15 @@ final class CachingStream implements StreamInterface
 
 
     public function read($length): string
     public function read($length): string
     {
     {
+        if (!\is_int($length)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing %s to StreamInterface::read() is deprecated; guzzlehttp/psr7 3.0 requires int for $length.',
+                \get_debug_type($length)
+            );
+        }
+
         // Perform a regular read on any previously read data from the buffer
         // Perform a regular read on any previously read data from the buffer
         $data = $this->stream->read($length);
         $data = $this->stream->read($length);
         $remaining = $length - strlen($data);
         $remaining = $length - strlen($data);
@@ -109,7 +151,11 @@ final class CachingStream implements StreamInterface
             }
             }
 
 
             $data .= $remoteData;
             $data .= $remoteData;
-            $this->stream->write($remoteData);
+
+            // A short cache write would silently corrupt later replays, so fail loudly.
+            if ($this->stream->write($remoteData) !== strlen($remoteData)) {
+                throw new \RuntimeException('Unable to cache the entire read from the remote stream');
+            }
         }
         }
 
 
         return $data;
         return $data;
@@ -117,6 +163,15 @@ final class CachingStream implements StreamInterface
 
 
     public function write($string): int
     public function write($string): int
     {
     {
+        if (!\is_string($string)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing %s to StreamInterface::write() is deprecated; guzzlehttp/psr7 3.0 requires string for $string.',
+                \get_debug_type($string)
+            );
+        }
+
         // When appending to the end of the currently read stream, you'll want
         // When appending to the end of the currently read stream, you'll want
         // to skip bytes from being read from the remote stream to emulate
         // to skip bytes from being read from the remote stream to emulate
         // other stream wrappers. Basically replacing bytes of data of a fixed
         // other stream wrappers. Basically replacing bytes of data of a fixed
@@ -134,6 +189,23 @@ final class CachingStream implements StreamInterface
         return $this->stream->eof() && $this->remoteStream->eof();
         return $this->stream->eof() && $this->remoteStream->eof();
     }
     }
 
 
+    public function detach()
+    {
+        if ($this->detached) {
+            return null;
+        }
+
+        $position = $this->tell();
+
+        $this->cacheEntireStream();
+        $this->stream->seek($position);
+
+        $resource = $this->stream->detach();
+        $this->detached = true;
+
+        return $resource;
+    }
+
     /**
     /**
      * Close both the remote stream and buffer stream
      * Close both the remote stream and buffer stream
      */
      */
@@ -141,6 +213,7 @@ final class CachingStream implements StreamInterface
     {
     {
         $this->remoteStream->close();
         $this->remoteStream->close();
         $this->stream->close();
         $this->stream->close();
+        $this->detached = true;
     }
     }
 
 
     private function cacheEntireStream(): int
     private function cacheEntireStream(): int

+ 9 - 0
modules/boonex/xero/plugins/guzzlehttp/psr7/src/DroppingStream.php

@@ -32,6 +32,15 @@ final class DroppingStream implements StreamInterface
 
 
     public function write($string): int
     public function write($string): int
     {
     {
+        if (!\is_string($string)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing %s to StreamInterface::write() is deprecated; guzzlehttp/psr7 3.0 requires string for $string.',
+                \get_debug_type($string)
+            );
+        }
+
         $diff = $this->maxLength - $this->stream->getSize();
         $diff = $this->maxLength - $this->stream->getSize();
 
 
         // Begin returning 0 when the underlying stream is too large.
         // Begin returning 0 when the underlying stream is too large.

+ 48 - 3
modules/boonex/xero/plugins/guzzlehttp/psr7/src/FnStream.php

@@ -7,7 +7,7 @@ namespace GuzzleHttp\Psr7;
 use Psr\Http\Message\StreamInterface;
 use Psr\Http\Message\StreamInterface;
 
 
 /**
 /**
- * Compose stream implementations based on a hash of functions.
+ * Compose stream implementations based on a hash of callables.
  *
  *
  * Allows for easy testing and extension of a provided stream without needing
  * Allows for easy testing and extension of a provided stream without needing
  * to create a concrete class for a simple extension point.
  * to create a concrete class for a simple extension point.
@@ -31,7 +31,7 @@ final class FnStream implements StreamInterface
     {
     {
         $this->methods = $methods;
         $this->methods = $methods;
 
 
-        // Create the functions on the class
+        // Create the callables on the class
         foreach ($methods as $name => $fn) {
         foreach ($methods as $name => $fn) {
             $this->{'_fn_'.$name} = $fn;
             $this->{'_fn_'.$name} = $fn;
         }
         }
@@ -73,7 +73,7 @@ final class FnStream implements StreamInterface
      * specific method calls.
      * specific method calls.
      *
      *
      * @param StreamInterface         $stream  Stream to decorate
      * @param StreamInterface         $stream  Stream to decorate
-     * @param array<string, callable> $methods Hash of method name to a closure
+     * @param array<string, callable> $methods Hash of method name to a callable
      *
      *
      * @return FnStream
      * @return FnStream
      */
      */
@@ -142,6 +142,24 @@ final class FnStream implements StreamInterface
 
 
     public function seek($offset, $whence = SEEK_SET): void
     public function seek($offset, $whence = SEEK_SET): void
     {
     {
+        if (!\is_int($offset)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $offset.',
+                \get_debug_type($offset)
+            );
+        }
+
+        if (!\is_int($whence)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $whence.',
+                \get_debug_type($whence)
+            );
+        }
+
         ($this->_fn_seek)($offset, $whence);
         ($this->_fn_seek)($offset, $whence);
     }
     }
 
 
@@ -152,6 +170,15 @@ final class FnStream implements StreamInterface
 
 
     public function write($string): int
     public function write($string): int
     {
     {
+        if (!\is_string($string)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing %s to StreamInterface::write() is deprecated; guzzlehttp/psr7 3.0 requires string for $string.',
+                \get_debug_type($string)
+            );
+        }
+
         return ($this->_fn_write)($string);
         return ($this->_fn_write)($string);
     }
     }
 
 
@@ -162,6 +189,15 @@ final class FnStream implements StreamInterface
 
 
     public function read($length): string
     public function read($length): string
     {
     {
+        if (!\is_int($length)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing %s to StreamInterface::read() is deprecated; guzzlehttp/psr7 3.0 requires int for $length.',
+                \get_debug_type($length)
+            );
+        }
+
         return ($this->_fn_read)($length);
         return ($this->_fn_read)($length);
     }
     }
 
 
@@ -175,6 +211,15 @@ final class FnStream implements StreamInterface
      */
      */
     public function getMetadata($key = null)
     public function getMetadata($key = null)
     {
     {
+        if ($key !== null && !\is_string($key)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing %s to StreamInterface::getMetadata() is deprecated; guzzlehttp/psr7 3.0 requires string|null for $key.',
+                \get_debug_type($key)
+            );
+        }
+
         return ($this->_fn_getMetadata)($key);
         return ($this->_fn_getMetadata)($key);
     }
     }
 }
 }

+ 49 - 3
modules/boonex/xero/plugins/guzzlehttp/psr7/src/Header.php

@@ -22,7 +22,7 @@ final class Header
         foreach ((array) $header as $value) {
         foreach ((array) $header as $value) {
             foreach (self::splitList($value) as $val) {
             foreach (self::splitList($value) as $val) {
                 $part = [];
                 $part = [];
-                foreach (preg_split('/;(?=([^"]*"[^"]*")*[^"]*$)/', $val) ?: [] as $kvp) {
+                foreach (self::splitParameters($val) as $kvp) {
                     if (preg_match_all('/<[^>]+>|[^=]+/', $kvp, $matches)) {
                     if (preg_match_all('/<[^>]+>|[^=]+/', $kvp, $matches)) {
                         $m = $matches[0];
                         $m = $matches[0];
                         if (isset($m[1])) {
                         if (isset($m[1])) {
@@ -41,6 +41,50 @@ final class Header
         return $params;
         return $params;
     }
     }
 
 
+    /**
+     * Split a header value into semicolon-separated parameters.
+     *
+     * @return string[]
+     */
+    private static function splitParameters(string $value): array
+    {
+        $values = [];
+        $start = 0;
+        $isQuoted = false;
+        $isEscaped = false;
+
+        for ($i = 0, $max = \strlen($value); $i < $max; ++$i) {
+            $char = $value[$i];
+
+            if ($isEscaped) {
+                $isEscaped = false;
+
+                continue;
+            }
+
+            if ($isQuoted && $char === '\\') {
+                $isEscaped = true;
+
+                continue;
+            }
+
+            if ($char === '"') {
+                $isQuoted = !$isQuoted;
+
+                continue;
+            }
+
+            if (!$isQuoted && $char === ';') {
+                $values[] = \substr($value, $start, $i - $start);
+                $start = $i + 1;
+            }
+        }
+
+        $values[] = \substr($value, $start);
+
+        return $values;
+    }
+
     /**
     /**
      * Converts an array of header values that may contain comma separated
      * Converts an array of header values that may contain comma separated
      * headers into an array of headers with no comma separated values.
      * headers into an array of headers with no comma separated values.
@@ -51,6 +95,8 @@ final class Header
      */
      */
     public static function normalize($header): array
     public static function normalize($header): array
     {
     {
+        \trigger_deprecation('guzzlehttp/psr7', '2.3', 'Header::normalize() is deprecated and will be removed in guzzlehttp/psr7 3.0. Use Header::splitList() instead.');
+
         $result = [];
         $result = [];
         foreach ((array) $header as $value) {
         foreach ((array) $header as $value) {
             foreach (self::splitList($value) as $parsed) {
             foreach (self::splitList($value) as $parsed) {
@@ -98,7 +144,7 @@ final class Header
                 }
                 }
 
 
                 if (!$isQuoted && $value[$i] === ',') {
                 if (!$isQuoted && $value[$i] === ',') {
-                    $v = \trim($v);
+                    $v = \trim($v, " \n\r\t\0\x0B");
                     if ($v !== '') {
                     if ($v !== '') {
                         $result[] = $v;
                         $result[] = $v;
                     }
                     }
@@ -123,7 +169,7 @@ final class Header
                 $v .= $value[$i];
                 $v .= $value[$i];
             }
             }
 
 
-            $v = \trim($v);
+            $v = \trim($v, " \n\r\t\0\x0B");
             if ($v !== '') {
             if ($v !== '') {
                 $result[] = $v;
                 $result[] = $v;
             }
             }

+ 35 - 4
modules/boonex/xero/plugins/guzzlehttp/psr7/src/LimitStream.php

@@ -61,11 +61,15 @@ final class LimitStream implements StreamInterface
     {
     {
         if (null === ($length = $this->stream->getSize())) {
         if (null === ($length = $this->stream->getSize())) {
             return null;
             return null;
-        } elseif ($this->limit === -1) {
-            return $length - $this->offset;
-        } else {
-            return min($this->limit, $length - $this->offset);
         }
         }
+
+        $size = $length - $this->offset;
+
+        if ($this->limit !== -1) {
+            $size = min($this->limit, $size);
+        }
+
+        return max(0, $size);
     }
     }
 
 
     /**
     /**
@@ -73,6 +77,24 @@ final class LimitStream implements StreamInterface
      */
      */
     public function seek($offset, $whence = SEEK_SET): void
     public function seek($offset, $whence = SEEK_SET): void
     {
     {
+        if (!\is_int($offset)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $offset.',
+                \get_debug_type($offset)
+            );
+        }
+
+        if (!\is_int($whence)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $whence.',
+                \get_debug_type($whence)
+            );
+        }
+
         if ($whence !== SEEK_SET || $offset < 0) {
         if ($whence !== SEEK_SET || $offset < 0) {
             throw new \RuntimeException(sprintf(
             throw new \RuntimeException(sprintf(
                 'Cannot seek to offset %s with whence %s',
                 'Cannot seek to offset %s with whence %s',
@@ -139,6 +161,15 @@ final class LimitStream implements StreamInterface
 
 
     public function read($length): string
     public function read($length): string
     {
     {
+        if (!\is_int($length)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing %s to StreamInterface::read() is deprecated; guzzlehttp/psr7 3.0 requires int for $length.',
+                \get_debug_type($length)
+            );
+        }
+
         if ($this->limit === -1) {
         if ($this->limit === -1) {
             return $this->stream->read($length);
             return $this->stream->read($length);
         }
         }

+ 150 - 14
modules/boonex/xero/plugins/guzzlehttp/psr7/src/Message.php

@@ -19,7 +19,7 @@ final class Message
     {
     {
         if ($message instanceof RequestInterface) {
         if ($message instanceof RequestInterface) {
             $msg = trim($message->getMethod().' '
             $msg = trim($message->getMethod().' '
-                    .$message->getRequestTarget())
+                    .$message->getRequestTarget(), " \n\r\t\0\x0B")
                 .' HTTP/'.$message->getProtocolVersion();
                 .' HTTP/'.$message->getProtocolVersion();
             if (!$message->hasHeader('host')) {
             if (!$message->hasHeader('host')) {
                 $msg .= "\r\nHost: ".$message->getUri()->getHost();
                 $msg .= "\r\nHost: ".$message->getUri()->getHost();
@@ -33,7 +33,7 @@ final class Message
         }
         }
 
 
         foreach ($message->getHeaders() as $name => $values) {
         foreach ($message->getHeaders() as $name => $values) {
-            if (is_string($name) && strtolower($name) === 'set-cookie') {
+            if (is_string($name) && Utils::asciiToLower($name) === 'set-cookie') {
                 foreach ($values as $value) {
                 foreach ($values as $value) {
                     $msg .= "\r\n{$name}: ".$value;
                     $msg .= "\r\n{$name}: ".$value;
                 }
                 }
@@ -69,12 +69,17 @@ final class Message
 
 
         $body->rewind();
         $body->rewind();
         $summary = $body->read($truncateAt);
         $summary = $body->read($truncateAt);
-        $body->rewind();
 
 
         if ($size > $truncateAt) {
         if ($size > $truncateAt) {
+            if (preg_match('//u', $summary) !== 1) {
+                $summary = self::trimTrailingIncompleteUtf8Character($summary, $body->read(3));
+            }
+
             $summary .= ' (truncated...)';
             $summary .= ' (truncated...)';
         }
         }
 
 
+        $body->rewind();
+
         // Matches any printable character, including unicode characters:
         // Matches any printable character, including unicode characters:
         // letters, marks, numbers, punctuation, spacing, and separators.
         // letters, marks, numbers, punctuation, spacing, and separators.
         if (preg_match('/[^\pL\pM\pN\pP\pS\pZ\n\r\t]/u', $summary) !== 0) {
         if (preg_match('/[^\pL\pM\pN\pP\pS\pZ\n\r\t]/u', $summary) !== 0) {
@@ -84,6 +89,60 @@ final class Message
         return $summary;
         return $summary;
     }
     }
 
 
+    /**
+     * Trims a partial UTF-8 character from the end of a truncated string.
+     */
+    private static function trimTrailingIncompleteUtf8Character(string $summary, string $lookahead): string
+    {
+        $length = strlen($summary);
+
+        if ($length === 0) {
+            return $summary;
+        }
+
+        $start = $length - 1;
+
+        while ($start >= 0) {
+            $byte = ord($summary[$start]);
+
+            if ($byte < 0x80 || $byte > 0xBF) {
+                break;
+            }
+
+            --$start;
+        }
+
+        if ($start < 0) {
+            return $summary;
+        }
+
+        $lead = ord($summary[$start]);
+
+        if ($lead >= 0xC2 && $lead <= 0xDF) {
+            $expectedLength = 2;
+        } elseif ($lead >= 0xE0 && $lead <= 0xEF) {
+            $expectedLength = 3;
+        } elseif ($lead >= 0xF0 && $lead <= 0xF4) {
+            $expectedLength = 4;
+        } else {
+            return $summary;
+        }
+
+        $availableLength = $length - $start;
+
+        if ($availableLength >= $expectedLength) {
+            return $summary;
+        }
+
+        $sequence = substr($summary, $start).substr($lookahead, 0, $expectedLength - $availableLength);
+
+        if (strlen($sequence) !== $expectedLength || preg_match('//u', $sequence) !== 1) {
+            return $summary;
+        }
+
+        return substr($summary, 0, $start);
+    }
+
     /**
     /**
      * Attempts to rewind a message body and throws an exception on failure.
      * Attempts to rewind a message body and throws an exception on failure.
      *
      *
@@ -122,7 +181,11 @@ final class Message
 
 
         $messageParts = preg_split("/\r?\n\r?\n/", $message, 2);
         $messageParts = preg_split("/\r?\n\r?\n/", $message, 2);
 
 
-        if ($messageParts === false || count($messageParts) !== 2) {
+        if ($messageParts === false) {
+            throw new \RuntimeException('Unable to split HTTP message: '.preg_last_error_msg());
+        }
+
+        if (count($messageParts) !== 2) {
             throw new \InvalidArgumentException('Invalid message: Missing header delimiter');
             throw new \InvalidArgumentException('Invalid message: Missing header delimiter');
         }
         }
 
 
@@ -130,24 +193,48 @@ final class Message
         $rawHeaders .= "\r\n"; // Put back the delimiter we split previously
         $rawHeaders .= "\r\n"; // Put back the delimiter we split previously
         $headerParts = preg_split("/\r?\n/", $rawHeaders, 2);
         $headerParts = preg_split("/\r?\n/", $rawHeaders, 2);
 
 
-        if ($headerParts === false || count($headerParts) !== 2) {
+        if ($headerParts === false) {
+            throw new \RuntimeException('Unable to split HTTP message headers: '.preg_last_error_msg());
+        }
+
+        if (count($headerParts) !== 2) {
             throw new \InvalidArgumentException('Invalid message: Missing status line');
             throw new \InvalidArgumentException('Invalid message: Missing status line');
         }
         }
 
 
         [$startLine, $rawHeaders] = $headerParts;
         [$startLine, $rawHeaders] = $headerParts;
 
 
-        if (preg_match("/(?:^HTTP\/|^[A-Z]+ \S+ HTTP\/)(\d+(?:\.\d+)?)/i", $startLine, $matches) && $matches[1] === '1.0') {
+        $versionMatch = preg_match("/(?:^HTTP\/|^[A-Z]+ \S+ HTTP\/)(\d+(?:\.\d+)?)/i", $startLine, $matches);
+
+        if ($versionMatch === false) {
+            throw new \RuntimeException('Unable to parse HTTP start line: '.preg_last_error_msg());
+        }
+
+        if ($versionMatch === 1 && $matches[1] === '1.0') {
             // Header folding is deprecated for HTTP/1.1, but allowed in HTTP/1.0
             // Header folding is deprecated for HTTP/1.1, but allowed in HTTP/1.0
             $rawHeaders = preg_replace(Rfc7230::HEADER_FOLD_REGEX, ' ', $rawHeaders);
             $rawHeaders = preg_replace(Rfc7230::HEADER_FOLD_REGEX, ' ', $rawHeaders);
+
+            if ($rawHeaders === null) {
+                throw new \RuntimeException('Unable to unfold HTTP headers: '.preg_last_error_msg());
+            }
         }
         }
 
 
         /** @var array[] $headerLines */
         /** @var array[] $headerLines */
         $count = preg_match_all(Rfc7230::HEADER_REGEX, $rawHeaders, $headerLines, PREG_SET_ORDER);
         $count = preg_match_all(Rfc7230::HEADER_REGEX, $rawHeaders, $headerLines, PREG_SET_ORDER);
 
 
+        if ($count === false) {
+            throw new \RuntimeException('Unable to parse HTTP headers: '.preg_last_error_msg());
+        }
+
         // If these aren't the same, then one line didn't match and there's an invalid header.
         // If these aren't the same, then one line didn't match and there's an invalid header.
         if ($count !== substr_count($rawHeaders, "\n")) {
         if ($count !== substr_count($rawHeaders, "\n")) {
             // Folding is deprecated, see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.4
             // Folding is deprecated, see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.4
-            if (preg_match(Rfc7230::HEADER_FOLD_REGEX, $rawHeaders)) {
+            $hasFoldedHeader = preg_match(Rfc7230::HEADER_FOLD_REGEX, $rawHeaders);
+
+            if ($hasFoldedHeader === false) {
+                throw new \RuntimeException('Unable to inspect HTTP header folding: '.preg_last_error_msg());
+            }
+
+            if ($hasFoldedHeader === 1) {
                 throw new \InvalidArgumentException('Invalid header syntax: Obsolete line folding');
                 throw new \InvalidArgumentException('Invalid header syntax: Obsolete line folding');
             }
             }
 
 
@@ -174,23 +261,52 @@ final class Message
      * @param array  $headers Array of headers (each value an array).
      * @param array  $headers Array of headers (each value an array).
      */
      */
     public static function parseRequestUri(string $path, array $headers): string
     public static function parseRequestUri(string $path, array $headers): string
+    {
+        $host = self::getHostFromHeaders($headers);
+
+        // If no host is found, then a full URI cannot be constructed.
+        // Collapse leading slashes so an origin-form target cannot be
+        // parsed as a network-path reference with its own authority.
+        if ($host === null) {
+            return self::normalizePathForOriginForm($path);
+        }
+
+        $scheme = substr($host, -4) === ':443' ? 'https' : 'http';
+
+        return $scheme.'://'.$host.'/'.ltrim($path, '/');
+    }
+
+    private static function normalizePathForOriginForm(string $path): string
+    {
+        if (0 === strpos($path, '//')) {
+            return '/'.ltrim($path, '/');
+        }
+
+        return $path;
+    }
+
+    /**
+     * @param array $headers Array of headers (each value an array).
+     */
+    private static function getHostFromHeaders(array $headers): ?string
     {
     {
         $hostKey = array_filter(array_keys($headers), function ($k) {
         $hostKey = array_filter(array_keys($headers), function ($k) {
             // Numeric array keys are converted to int by PHP.
             // Numeric array keys are converted to int by PHP.
             $k = (string) $k;
             $k = (string) $k;
 
 
-            return strtolower($k) === 'host';
+            return Utils::asciiToLower($k) === 'host';
         });
         });
 
 
-        // If no host is found, then a full URI cannot be constructed.
         if (!$hostKey) {
         if (!$hostKey) {
-            return $path;
+            return null;
         }
         }
 
 
         $host = $headers[reset($hostKey)][0];
         $host = $headers[reset($hostKey)][0];
-        $scheme = substr($host, -4) === ':443' ? 'https' : 'http';
+        if (!is_string($host) || Rfc7230::parseHostHeader($host) === null) {
+            throw new \InvalidArgumentException('Invalid request string');
+        }
 
 
-        return $scheme.'://'.$host.'/'.ltrim($path, '/');
+        return $host;
     }
     }
 
 
     /**
     /**
@@ -201,8 +317,18 @@ final class Message
     public static function parseRequest(string $message): RequestInterface
     public static function parseRequest(string $message): RequestInterface
     {
     {
         $data = self::parseMessage($message);
         $data = self::parseMessage($message);
+        if (strpbrk($data['start-line'], "\r\n") !== false) {
+            throw new \InvalidArgumentException('Invalid request string');
+        }
+
         $matches = [];
         $matches = [];
-        if (!preg_match('/^[\S]+\s+([a-zA-Z]+:\/\/|\/).*/', $data['start-line'], $matches)) {
+        $requestStartLineMatch = preg_match('/^[\S]+\s+([a-zA-Z]+:\/\/|\/).*/', $data['start-line'], $matches);
+
+        if ($requestStartLineMatch === false) {
+            throw new \RuntimeException('Unable to parse request start line: '.preg_last_error_msg());
+        }
+
+        if ($requestStartLineMatch === 0) {
             throw new \InvalidArgumentException('Invalid request string');
             throw new \InvalidArgumentException('Invalid request string');
         }
         }
         $parts = explode(' ', $data['start-line'], 3);
         $parts = explode(' ', $data['start-line'], 3);
@@ -227,10 +353,20 @@ final class Message
     public static function parseResponse(string $message): ResponseInterface
     public static function parseResponse(string $message): ResponseInterface
     {
     {
         $data = self::parseMessage($message);
         $data = self::parseMessage($message);
+        if (strpbrk($data['start-line'], "\r\n") !== false) {
+            throw new \InvalidArgumentException('Invalid response string');
+        }
+
         // According to https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2
         // According to https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2
         // the space between status-code and reason-phrase is required. But
         // the space between status-code and reason-phrase is required. But
         // browsers accept responses without space and reason as well.
         // browsers accept responses without space and reason as well.
-        if (!preg_match('/^HTTP\/.* [0-9]{3}( .*|$)/', $data['start-line'])) {
+        $responseStartLineMatch = preg_match('/^HTTP\/.* [0-9]{3}( .*|$)/D', $data['start-line']);
+
+        if ($responseStartLineMatch === false) {
+            throw new \RuntimeException('Unable to parse response start line: '.preg_last_error_msg());
+        }
+
+        if ($responseStartLineMatch === 0) {
             throw new \InvalidArgumentException('Invalid response string: '.$data['start-line']);
             throw new \InvalidArgumentException('Invalid response string: '.$data['start-line']);
         }
         }
         $parts = explode(' ', $data['start-line'], 3);
         $parts = explode(' ', $data['start-line'], 3);

+ 103 - 10
modules/boonex/xero/plugins/guzzlehttp/psr7/src/MessageTrait.php

@@ -29,8 +29,22 @@ trait MessageTrait
         return $this->protocol;
         return $this->protocol;
     }
     }
 
 
+    /**
+     * @return static
+     */
     public function withProtocolVersion($version): MessageInterface
     public function withProtocolVersion($version): MessageInterface
     {
     {
+        if (!\is_string($version)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing %s to MessageInterface::withProtocolVersion() is deprecated; guzzlehttp/psr7 3.0 requires string.',
+                \get_debug_type($version)
+            );
+        }
+
+        $this->assertProtocolVersion($version);
+
         if ($this->protocol === $version) {
         if ($this->protocol === $version) {
             return $this;
             return $this;
         }
         }
@@ -48,12 +62,12 @@ trait MessageTrait
 
 
     public function hasHeader($header): bool
     public function hasHeader($header): bool
     {
     {
-        return isset($this->headerNames[strtolower($header)]);
+        return isset($this->headerNames[Utils::asciiToLower($header)]);
     }
     }
 
 
     public function getHeader($header): array
     public function getHeader($header): array
     {
     {
-        $header = strtolower($header);
+        $header = Utils::asciiToLower($header);
 
 
         if (!isset($this->headerNames[$header])) {
         if (!isset($this->headerNames[$header])) {
             return [];
             return [];
@@ -69,11 +83,27 @@ trait MessageTrait
         return implode(', ', $this->getHeader($header));
         return implode(', ', $this->getHeader($header));
     }
     }
 
 
+    /**
+     * @return static
+     */
     public function withHeader($header, $value): MessageInterface
     public function withHeader($header, $value): MessageInterface
     {
     {
         $this->assertHeader($header);
         $this->assertHeader($header);
+        $values = \is_array($value) ? $value : [$value];
+        foreach ($values as $item) {
+            if (!\is_string($item) && (\is_scalar($item) || $item === null)) {
+                \trigger_deprecation(
+                    'guzzlehttp/psr7',
+                    '2.11',
+                    'Passing %s to MessageInterface::withHeader() is deprecated; guzzlehttp/psr7 3.0 requires string|string[].',
+                    \get_debug_type($item)
+                );
+
+                break;
+            }
+        }
         $value = $this->normalizeHeaderValue($value);
         $value = $this->normalizeHeaderValue($value);
-        $normalized = strtolower($header);
+        $normalized = Utils::asciiToLower($header);
 
 
         $new = clone $this;
         $new = clone $this;
         if (isset($new->headerNames[$normalized])) {
         if (isset($new->headerNames[$normalized])) {
@@ -85,11 +115,27 @@ trait MessageTrait
         return $new;
         return $new;
     }
     }
 
 
+    /**
+     * @return static
+     */
     public function withAddedHeader($header, $value): MessageInterface
     public function withAddedHeader($header, $value): MessageInterface
     {
     {
         $this->assertHeader($header);
         $this->assertHeader($header);
+        $values = \is_array($value) ? $value : [$value];
+        foreach ($values as $item) {
+            if (!\is_string($item) && (\is_scalar($item) || $item === null)) {
+                \trigger_deprecation(
+                    'guzzlehttp/psr7',
+                    '2.11',
+                    'Passing %s to MessageInterface::withAddedHeader() is deprecated; guzzlehttp/psr7 3.0 requires string|string[].',
+                    \get_debug_type($item)
+                );
+
+                break;
+            }
+        }
         $value = $this->normalizeHeaderValue($value);
         $value = $this->normalizeHeaderValue($value);
-        $normalized = strtolower($header);
+        $normalized = Utils::asciiToLower($header);
 
 
         $new = clone $this;
         $new = clone $this;
         if (isset($new->headerNames[$normalized])) {
         if (isset($new->headerNames[$normalized])) {
@@ -103,9 +149,12 @@ trait MessageTrait
         return $new;
         return $new;
     }
     }
 
 
+    /**
+     * @return static
+     */
     public function withoutHeader($header): MessageInterface
     public function withoutHeader($header): MessageInterface
     {
     {
-        $normalized = strtolower($header);
+        $normalized = Utils::asciiToLower($header);
 
 
         if (!isset($this->headerNames[$normalized])) {
         if (!isset($this->headerNames[$normalized])) {
             return $this;
             return $this;
@@ -128,6 +177,9 @@ trait MessageTrait
         return $this->stream;
         return $this->stream;
     }
     }
 
 
+    /**
+     * @return static
+     */
     public function withBody(StreamInterface $body): MessageInterface
     public function withBody(StreamInterface $body): MessageInterface
     {
     {
         if ($body === $this->stream) {
         if ($body === $this->stream) {
@@ -151,8 +203,22 @@ trait MessageTrait
             $header = (string) $header;
             $header = (string) $header;
 
 
             $this->assertHeader($header);
             $this->assertHeader($header);
+            $values = \is_array($value) ? $value : [$value];
+            foreach ($values as $item) {
+                if (!\is_string($item) && (\is_scalar($item) || $item === null)) {
+                    \trigger_deprecation(
+                        'guzzlehttp/psr7',
+                        '2.11',
+                        'Passing %s to %s::__construct() is deprecated; guzzlehttp/psr7 3.0 requires string|string[].',
+                        \get_debug_type($item),
+                        static::class
+                    );
+
+                    break;
+                }
+            }
             $value = $this->normalizeHeaderValue($value);
             $value = $this->normalizeHeaderValue($value);
-            $normalized = strtolower($header);
+            $normalized = Utils::asciiToLower($header);
             if (isset($this->headerNames[$normalized])) {
             if (isset($this->headerNames[$normalized])) {
                 $header = $this->headerNames[$normalized];
                 $header = $this->headerNames[$normalized];
                 $this->headers[$header] = array_merge($this->headers[$header], $value);
                 $this->headers[$header] = array_merge($this->headers[$header], $value);
@@ -170,12 +236,16 @@ trait MessageTrait
      */
      */
     private function normalizeHeaderValue($value): array
     private function normalizeHeaderValue($value): array
     {
     {
-        if (!is_array($value)) {
-            return $this->trimAndValidateHeaderValues([$value]);
+        if (is_array($value) && $value === []) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing an empty array as a header value is deprecated; guzzlehttp/psr7 3.0 rejects empty header value arrays.'
+            );
         }
         }
 
 
-        if (count($value) === 0) {
-            throw new \InvalidArgumentException('Header value can not be an empty array.');
+        if (!is_array($value)) {
+            return $this->trimAndValidateHeaderValues([$value]);
         }
         }
 
 
         return $this->trimAndValidateHeaderValues($value);
         return $this->trimAndValidateHeaderValues($value);
@@ -205,6 +275,12 @@ trait MessageTrait
                 ));
                 ));
             }
             }
 
 
+            // Convert non-finite floats explicitly, as implicit coercion of
+            // NAN emits a warning on PHP 8.5.
+            if (is_float($value) && !is_finite($value)) {
+                $value = is_nan($value) ? 'NAN' : ($value > 0 ? 'INF' : '-INF');
+            }
+
             $trimmed = trim((string) $value, " \t");
             $trimmed = trim((string) $value, " \t");
             $this->assertValue($trimmed);
             $this->assertValue($trimmed);
 
 
@@ -233,6 +309,23 @@ trait MessageTrait
         }
         }
     }
     }
 
 
+    /**
+     * @param mixed $version
+     */
+    private function assertProtocolVersion($version): void
+    {
+        if (is_string($version)) {
+            $this->assertNoLineSeparators($version, 'Protocol version');
+        }
+    }
+
+    private function assertNoLineSeparators(string $value, string $field): void
+    {
+        if (strpbrk($value, "\r\n") !== false) {
+            throw new \InvalidArgumentException($field.' must not contain CR or LF characters.');
+        }
+    }
+
     /**
     /**
      * @see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2
      * @see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2
      *
      *

+ 120 - 74
modules/boonex/xero/plugins/guzzlehttp/psr7/src/MimeType.php

@@ -7,22 +7,23 @@ namespace GuzzleHttp\Psr7;
 final class MimeType
 final class MimeType
 {
 {
     private const MIME_TYPES = [
     private const MIME_TYPES = [
+        '123' => 'application/vnd.lotus-1-2-3',
         '1km' => 'application/vnd.1000minds.decision-model+xml',
         '1km' => 'application/vnd.1000minds.decision-model+xml',
+        '210' => 'model/step',
         '3dml' => 'text/vnd.in3d.3dml',
         '3dml' => 'text/vnd.in3d.3dml',
         '3ds' => 'image/x-3ds',
         '3ds' => 'image/x-3ds',
         '3g2' => 'video/3gpp2',
         '3g2' => 'video/3gpp2',
-        '3gp' => 'video/3gp',
+        '3gp' => 'video/3gpp',
         '3gpp' => 'video/3gpp',
         '3gpp' => 'video/3gpp',
         '3mf' => 'model/3mf',
         '3mf' => 'model/3mf',
         '7z' => 'application/x-7z-compressed',
         '7z' => 'application/x-7z-compressed',
         '7zip' => 'application/x-7z-compressed',
         '7zip' => 'application/x-7z-compressed',
-        '123' => 'application/vnd.lotus-1-2-3',
         'aab' => 'application/x-authorware-bin',
         'aab' => 'application/x-authorware-bin',
         'aac' => 'audio/aac',
         'aac' => 'audio/aac',
         'aam' => 'application/x-authorware-map',
         'aam' => 'application/x-authorware-map',
         'aas' => 'application/x-authorware-seg',
         'aas' => 'application/x-authorware-seg',
         'abw' => 'application/x-abiword',
         'abw' => 'application/x-abiword',
-        'ac' => 'application/vnd.nokia.n-gage.ac+xml',
+        'ac' => 'application/pkix-attr-cert',
         'ac3' => 'audio/ac3',
         'ac3' => 'audio/ac3',
         'acc' => 'application/vnd.americandynamics.acc',
         'acc' => 'application/vnd.americandynamics.acc',
         'ace' => 'application/x-ace-compressed',
         'ace' => 'application/x-ace-compressed',
@@ -35,7 +36,7 @@ final class MimeType
         'afp' => 'application/vnd.ibm.modcap',
         'afp' => 'application/vnd.ibm.modcap',
         'age' => 'application/vnd.age',
         'age' => 'application/vnd.age',
         'ahead' => 'application/vnd.ahead.space',
         'ahead' => 'application/vnd.ahead.space',
-        'ai' => 'application/pdf',
+        'ai' => 'application/postscript',
         'aif' => 'audio/x-aiff',
         'aif' => 'audio/x-aiff',
         'aifc' => 'audio/x-aiff',
         'aifc' => 'audio/x-aiff',
         'aiff' => 'audio/x-aiff',
         'aiff' => 'audio/x-aiff',
@@ -55,7 +56,7 @@ final class MimeType
         'apr' => 'application/vnd.lotus-approach',
         'apr' => 'application/vnd.lotus-approach',
         'arc' => 'application/x-freearc',
         'arc' => 'application/x-freearc',
         'arj' => 'application/x-arj',
         'arj' => 'application/x-arj',
-        'asc' => 'application/pgp-signature',
+        'asc' => 'application/pgp-keys',
         'asf' => 'video/x-ms-asf',
         'asf' => 'video/x-ms-asf',
         'asm' => 'text/x-asm',
         'asm' => 'text/x-asm',
         'aso' => 'application/vnd.accpac.simply.aso',
         'aso' => 'application/vnd.accpac.simply.aso',
@@ -66,7 +67,7 @@ final class MimeType
         'atomdeleted' => 'application/atomdeleted+xml',
         'atomdeleted' => 'application/atomdeleted+xml',
         'atomsvc' => 'application/atomsvc+xml',
         'atomsvc' => 'application/atomsvc+xml',
         'atx' => 'application/vnd.antix.game-component',
         'atx' => 'application/vnd.antix.game-component',
-        'au' => 'audio/x-au',
+        'au' => 'audio/basic',
         'avci' => 'image/avci',
         'avci' => 'image/avci',
         'avcs' => 'image/avcs',
         'avcs' => 'image/avcs',
         'avi' => 'video/x-msvideo',
         'avi' => 'video/x-msvideo',
@@ -77,15 +78,18 @@ final class MimeType
         'azv' => 'image/vnd.airzip.accelerator.azv',
         'azv' => 'image/vnd.airzip.accelerator.azv',
         'azw' => 'application/vnd.amazon.ebook',
         'azw' => 'application/vnd.amazon.ebook',
         'b16' => 'image/vnd.pco.b16',
         'b16' => 'image/vnd.pco.b16',
+        'bary' => 'model/vnd.bary',
         'bat' => 'application/x-msdownload',
         'bat' => 'application/x-msdownload',
         'bcpio' => 'application/x-bcpio',
         'bcpio' => 'application/x-bcpio',
         'bdf' => 'application/x-font-bdf',
         'bdf' => 'application/x-font-bdf',
         'bdm' => 'application/vnd.syncml.dm+wbxml',
         'bdm' => 'application/vnd.syncml.dm+wbxml',
-        'bdoc' => 'application/x-bdoc',
+        'bdo' => 'application/vnd.nato.bindingdataobject+xml',
+        'bdoc' => 'application/bdoc',
         'bed' => 'application/vnd.realvnc.bed',
         'bed' => 'application/vnd.realvnc.bed',
         'bh2' => 'application/vnd.fujitsu.oasysprs',
         'bh2' => 'application/vnd.fujitsu.oasysprs',
         'bin' => 'application/octet-stream',
         'bin' => 'application/octet-stream',
         'blb' => 'application/x-blorb',
         'blb' => 'application/x-blorb',
+        'blend' => 'application/x-blender',
         'blorb' => 'application/x-blorb',
         'blorb' => 'application/x-blorb',
         'bmi' => 'application/vnd.bmi',
         'bmi' => 'application/vnd.bmi',
         'bmml' => 'application/vnd.balsamiq.bmml+xml',
         'bmml' => 'application/vnd.balsamiq.bmml+xml',
@@ -95,6 +99,8 @@ final class MimeType
         'boz' => 'application/x-bzip2',
         'boz' => 'application/x-bzip2',
         'bpk' => 'application/octet-stream',
         'bpk' => 'application/octet-stream',
         'bpmn' => 'application/octet-stream',
         'bpmn' => 'application/octet-stream',
+        'brush' => 'application/vnd.procreate.brush',
+        'brushset' => 'application/vnd.procreate.brushset',
         'bsp' => 'model/vnd.valve.source.compiled-map',
         'bsp' => 'model/vnd.valve.source.compiled-map',
         'btf' => 'image/prs.btif',
         'btf' => 'image/prs.btif',
         'btif' => 'image/prs.btif',
         'btif' => 'image/prs.btif',
@@ -102,13 +108,13 @@ final class MimeType
         'bz' => 'application/x-bzip',
         'bz' => 'application/x-bzip',
         'bz2' => 'application/x-bzip2',
         'bz2' => 'application/x-bzip2',
         'c' => 'text/x-c',
         'c' => 'text/x-c',
+        'c11amc' => 'application/vnd.cluetrust.cartomobile-config',
+        'c11amz' => 'application/vnd.cluetrust.cartomobile-config-pkg',
         'c4d' => 'application/vnd.clonk.c4group',
         'c4d' => 'application/vnd.clonk.c4group',
         'c4f' => 'application/vnd.clonk.c4group',
         'c4f' => 'application/vnd.clonk.c4group',
         'c4g' => 'application/vnd.clonk.c4group',
         'c4g' => 'application/vnd.clonk.c4group',
         'c4p' => 'application/vnd.clonk.c4group',
         'c4p' => 'application/vnd.clonk.c4group',
         'c4u' => 'application/vnd.clonk.c4group',
         'c4u' => 'application/vnd.clonk.c4group',
-        'c11amc' => 'application/vnd.cluetrust.cartomobile-config',
-        'c11amz' => 'application/vnd.cluetrust.cartomobile-config-pkg',
         'cab' => 'application/vnd.ms-cab-compressed',
         'cab' => 'application/vnd.ms-cab-compressed',
         'caf' => 'audio/x-caf',
         'caf' => 'audio/x-caf',
         'cap' => 'application/vnd.tcpdump.pcap',
         'cap' => 'application/vnd.tcpdump.pcap',
@@ -132,7 +138,6 @@ final class MimeType
         'cdmid' => 'application/cdmi-domain',
         'cdmid' => 'application/cdmi-domain',
         'cdmio' => 'application/cdmi-object',
         'cdmio' => 'application/cdmi-object',
         'cdmiq' => 'application/cdmi-queue',
         'cdmiq' => 'application/cdmi-queue',
-        'cdr' => 'application/cdr',
         'cdx' => 'chemical/x-cdx',
         'cdx' => 'chemical/x-cdx',
         'cdxml' => 'application/vnd.chemdraw+xml',
         'cdxml' => 'application/vnd.chemdraw+xml',
         'cdy' => 'application/vnd.cinderella',
         'cdy' => 'application/vnd.cinderella',
@@ -147,7 +152,7 @@ final class MimeType
         'cil' => 'application/vnd.ms-artgalry',
         'cil' => 'application/vnd.ms-artgalry',
         'cjs' => 'application/node',
         'cjs' => 'application/node',
         'cla' => 'application/vnd.claymore',
         'cla' => 'application/vnd.claymore',
-        'class' => 'application/octet-stream',
+        'class' => 'application/java-vm',
         'cld' => 'model/vnd.cld',
         'cld' => 'model/vnd.cld',
         'clkk' => 'application/vnd.crick.clicker.keyboard',
         'clkk' => 'application/vnd.crick.clicker.keyboard',
         'clkp' => 'application/vnd.crick.clicker.palette',
         'clkp' => 'application/vnd.crick.clicker.palette',
@@ -194,6 +199,8 @@ final class MimeType
         'davmount' => 'application/davmount+xml',
         'davmount' => 'application/davmount+xml',
         'dbf' => 'application/vnd.dbf',
         'dbf' => 'application/vnd.dbf',
         'dbk' => 'application/docbook+xml',
         'dbk' => 'application/docbook+xml',
+        'dcm' => 'application/dicom',
+        'dcmp' => 'application/vnd.dcmp+xml',
         'dcr' => 'application/x-director',
         'dcr' => 'application/x-director',
         'dcurl' => 'text/vnd.curl.dcurl',
         'dcurl' => 'text/vnd.curl.dcurl',
         'dd2' => 'application/vnd.oma.dd2+xml',
         'dd2' => 'application/vnd.oma.dd2+xml',
@@ -221,19 +228,22 @@ final class MimeType
         'dmp' => 'application/vnd.tcpdump.pcap',
         'dmp' => 'application/vnd.tcpdump.pcap',
         'dms' => 'application/octet-stream',
         'dms' => 'application/octet-stream',
         'dna' => 'application/vnd.dna',
         'dna' => 'application/vnd.dna',
+        'dng' => 'image/x-adobe-dng',
         'doc' => 'application/msword',
         'doc' => 'application/msword',
-        'docm' => 'application/vnd.ms-word.template.macroEnabled.12',
+        'docm' => 'application/vnd.ms-word.document.macroenabled.12',
         'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
         'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
         'dot' => 'application/msword',
         'dot' => 'application/msword',
-        'dotm' => 'application/vnd.ms-word.template.macroEnabled.12',
+        'dotm' => 'application/vnd.ms-word.template.macroenabled.12',
         'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
         'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
         'dp' => 'application/vnd.osgi.dp',
         'dp' => 'application/vnd.osgi.dp',
         'dpg' => 'application/vnd.dpgraph',
         'dpg' => 'application/vnd.dpgraph',
         'dpx' => 'image/dpx',
         'dpx' => 'image/dpx',
         'dra' => 'audio/vnd.dra',
         'dra' => 'audio/vnd.dra',
         'drle' => 'image/dicom-rle',
         'drle' => 'image/dicom-rle',
+        'drm' => 'application/vnd.procreate.dream',
         'dsc' => 'text/prs.lines.tag',
         'dsc' => 'text/prs.lines.tag',
         'dssc' => 'application/dssc+der',
         'dssc' => 'application/dssc+der',
+        'dst' => 'application/octet-stream',
         'dtb' => 'application/x-dtbook+xml',
         'dtb' => 'application/x-dtbook+xml',
         'dtd' => 'application/xml-dtd',
         'dtd' => 'application/xml-dtd',
         'dts' => 'audio/vnd.dts',
         'dts' => 'audio/vnd.dts',
@@ -285,10 +295,12 @@ final class MimeType
         'f4v' => 'video/mp4',
         'f4v' => 'video/mp4',
         'f77' => 'text/x-fortran',
         'f77' => 'text/x-fortran',
         'f90' => 'text/x-fortran',
         'f90' => 'text/x-fortran',
+        'facti' => 'image/vnd.blockfact.facti',
         'fbs' => 'image/vnd.fastbidsheet',
         'fbs' => 'image/vnd.fastbidsheet',
+        'fbx' => 'application/vnd.autodesk.fbx',
         'fcdt' => 'application/vnd.adobe.formscentral.fcdt',
         'fcdt' => 'application/vnd.adobe.formscentral.fcdt',
         'fcs' => 'application/vnd.isac.fcs',
         'fcs' => 'application/vnd.isac.fcs',
-        'fdf' => 'application/vnd.fdf',
+        'fdf' => 'application/fdf',
         'fdt' => 'application/fdt+xml',
         'fdt' => 'application/fdt+xml',
         'fe_launch' => 'application/vnd.denovo.fcselayout-link',
         'fe_launch' => 'application/vnd.denovo.fcselayout-link',
         'fg5' => 'application/vnd.fujitsu.oasysgp',
         'fg5' => 'application/vnd.fujitsu.oasysgp',
@@ -330,21 +342,25 @@ final class MimeType
         'gca' => 'application/x-gca-compressed',
         'gca' => 'application/x-gca-compressed',
         'gdl' => 'model/vnd.gdl',
         'gdl' => 'model/vnd.gdl',
         'gdoc' => 'application/vnd.google-apps.document',
         'gdoc' => 'application/vnd.google-apps.document',
+        'gdraw' => 'application/vnd.google-apps.drawing',
         'ged' => 'text/vnd.familysearch.gedcom',
         'ged' => 'text/vnd.familysearch.gedcom',
         'geo' => 'application/vnd.dynageo',
         'geo' => 'application/vnd.dynageo',
         'geojson' => 'application/geo+json',
         'geojson' => 'application/geo+json',
         'gex' => 'application/vnd.geometry-explorer',
         'gex' => 'application/vnd.geometry-explorer',
+        'gform' => 'application/vnd.google-apps.form',
         'ggb' => 'application/vnd.geogebra.file',
         'ggb' => 'application/vnd.geogebra.file',
+        'ggs' => 'application/vnd.geogebra.slides',
         'ggt' => 'application/vnd.geogebra.tool',
         'ggt' => 'application/vnd.geogebra.tool',
         'ghf' => 'application/vnd.groove-help',
         'ghf' => 'application/vnd.groove-help',
         'gif' => 'image/gif',
         'gif' => 'image/gif',
         'gim' => 'application/vnd.groove-identity-message',
         'gim' => 'application/vnd.groove-identity-message',
+        'gjam' => 'application/vnd.google-apps.jam',
         'glb' => 'model/gltf-binary',
         'glb' => 'model/gltf-binary',
         'gltf' => 'model/gltf+json',
         'gltf' => 'model/gltf+json',
+        'gmap' => 'application/vnd.google-apps.map',
         'gml' => 'application/gml+xml',
         'gml' => 'application/gml+xml',
         'gmx' => 'application/vnd.gmx',
         'gmx' => 'application/vnd.gmx',
         'gnumeric' => 'application/x-gnumeric',
         'gnumeric' => 'application/x-gnumeric',
-        'gpg' => 'application/gpg-keys',
         'gph' => 'application/vnd.flographit',
         'gph' => 'application/vnd.flographit',
         'gpx' => 'application/gpx+xml',
         'gpx' => 'application/gpx+xml',
         'gqf' => 'application/vnd.grafeq',
         'gqf' => 'application/vnd.grafeq',
@@ -354,8 +370,10 @@ final class MimeType
         'gre' => 'application/vnd.geometry-explorer',
         'gre' => 'application/vnd.geometry-explorer',
         'grv' => 'application/vnd.groove-injector',
         'grv' => 'application/vnd.groove-injector',
         'grxml' => 'application/srgs+xml',
         'grxml' => 'application/srgs+xml',
+        'gscript' => 'application/vnd.google-apps.script',
         'gsf' => 'application/x-font-ghostscript',
         'gsf' => 'application/x-font-ghostscript',
         'gsheet' => 'application/vnd.google-apps.spreadsheet',
         'gsheet' => 'application/vnd.google-apps.spreadsheet',
+        'gsite' => 'application/vnd.google-apps.site',
         'gslides' => 'application/vnd.google-apps.presentation',
         'gslides' => 'application/vnd.google-apps.presentation',
         'gtar' => 'application/x-gtar',
         'gtar' => 'application/x-gtar',
         'gtm' => 'application/vnd.groove-tool-message',
         'gtm' => 'application/vnd.groove-tool-message',
@@ -387,7 +405,6 @@ final class MimeType
         'hpid' => 'application/vnd.hp-hpid',
         'hpid' => 'application/vnd.hp-hpid',
         'hps' => 'application/vnd.hp-hps',
         'hps' => 'application/vnd.hp-hps',
         'hqx' => 'application/mac-binhex40',
         'hqx' => 'application/mac-binhex40',
-        'hsj2' => 'image/hsj2',
         'htc' => 'text/x-component',
         'htc' => 'text/x-component',
         'htke' => 'application/vnd.kenameaapp',
         'htke' => 'application/vnd.kenameaapp',
         'htm' => 'text/html',
         'htm' => 'text/html',
@@ -399,7 +416,7 @@ final class MimeType
         'icc' => 'application/vnd.iccprofile',
         'icc' => 'application/vnd.iccprofile',
         'ice' => 'x-conference/x-cooltalk',
         'ice' => 'x-conference/x-cooltalk',
         'icm' => 'application/vnd.iccprofile',
         'icm' => 'application/vnd.iccprofile',
-        'ico' => 'image/x-icon',
+        'ico' => 'image/vnd.microsoft.icon',
         'ics' => 'text/calendar',
         'ics' => 'text/calendar',
         'ief' => 'image/ief',
         'ief' => 'image/ief',
         'ifb' => 'text/calendar',
         'ifb' => 'text/calendar',
@@ -414,6 +431,7 @@ final class MimeType
         'imp' => 'application/vnd.accpac.simply.imp',
         'imp' => 'application/vnd.accpac.simply.imp',
         'ims' => 'application/vnd.ms-ims',
         'ims' => 'application/vnd.ms-ims',
         'in' => 'text/plain',
         'in' => 'text/plain',
+        'indd' => 'application/x-indesign',
         'ini' => 'text/plain',
         'ini' => 'text/plain',
         'ink' => 'application/inkml+xml',
         'ink' => 'application/inkml+xml',
         'inkml' => 'application/inkml+xml',
         'inkml' => 'application/inkml+xml',
@@ -421,6 +439,7 @@ final class MimeType
         'iota' => 'application/vnd.astraea-software.iota',
         'iota' => 'application/vnd.astraea-software.iota',
         'ipfix' => 'application/ipfix',
         'ipfix' => 'application/ipfix',
         'ipk' => 'application/vnd.shana.informed.package',
         'ipk' => 'application/vnd.shana.informed.package',
+        'ipynb' => 'application/x-ipynb+json',
         'irm' => 'application/vnd.ibm.rights-management',
         'irm' => 'application/vnd.ibm.rights-management',
         'irp' => 'application/vnd.irepository.package+xml',
         'irp' => 'application/vnd.irepository.package+xml',
         'iso' => 'application/x-iso9660-image',
         'iso' => 'application/x-iso9660-image',
@@ -430,10 +449,13 @@ final class MimeType
         'ivu' => 'application/vnd.immervision-ivu',
         'ivu' => 'application/vnd.immervision-ivu',
         'jad' => 'text/vnd.sun.j2me.app-descriptor',
         'jad' => 'text/vnd.sun.j2me.app-descriptor',
         'jade' => 'text/jade',
         'jade' => 'text/jade',
+        'jaii' => 'image/jaii',
+        'jais' => 'image/jais',
         'jam' => 'application/vnd.jam',
         'jam' => 'application/vnd.jam',
         'jar' => 'application/java-archive',
         'jar' => 'application/java-archive',
         'jardiff' => 'application/x-java-archive-diff',
         'jardiff' => 'application/x-java-archive-diff',
         'java' => 'text/x-java-source',
         'java' => 'text/x-java-source',
+        'jfif' => 'image/jpeg',
         'jhc' => 'image/jphc',
         'jhc' => 'image/jphc',
         'jisp' => 'application/vnd.jisp',
         'jisp' => 'application/vnd.jisp',
         'jls' => 'image/jls',
         'jls' => 'image/jls',
@@ -447,18 +469,19 @@ final class MimeType
         'jpf' => 'image/jpx',
         'jpf' => 'image/jpx',
         'jpg' => 'image/jpeg',
         'jpg' => 'image/jpeg',
         'jpg2' => 'image/jp2',
         'jpg2' => 'image/jp2',
-        'jpgm' => 'video/jpm',
+        'jpgm' => 'image/jpm',
         'jpgv' => 'video/jpeg',
         'jpgv' => 'video/jpeg',
         'jph' => 'image/jph',
         'jph' => 'image/jph',
-        'jpm' => 'video/jpm',
+        'jpm' => 'image/jpm',
         'jpx' => 'image/jpx',
         'jpx' => 'image/jpx',
-        'js' => 'application/javascript',
+        'js' => 'text/javascript',
         'json' => 'application/json',
         'json' => 'application/json',
         'json5' => 'application/json5',
         'json5' => 'application/json5',
         'jsonld' => 'application/ld+json',
         'jsonld' => 'application/ld+json',
         'jsonml' => 'application/jsonml+json',
         'jsonml' => 'application/jsonml+json',
         'jsx' => 'text/jsx',
         'jsx' => 'text/jsx',
         'jt' => 'model/jt',
         'jt' => 'model/jt',
+        'jxl' => 'image/jxl',
         'jxr' => 'image/jxr',
         'jxr' => 'image/jxr',
         'jxra' => 'image/jxra',
         'jxra' => 'image/jxra',
         'jxrs' => 'image/jxrs',
         'jxrs' => 'image/jxrs',
@@ -468,9 +491,10 @@ final class MimeType
         'jxss' => 'image/jxss',
         'jxss' => 'image/jxss',
         'kar' => 'audio/midi',
         'kar' => 'audio/midi',
         'karbon' => 'application/vnd.kde.karbon',
         'karbon' => 'application/vnd.kde.karbon',
+        'kbl' => 'application/kbl+xml',
         'kdb' => 'application/octet-stream',
         'kdb' => 'application/octet-stream',
         'kdbx' => 'application/x-keepass2',
         'kdbx' => 'application/x-keepass2',
-        'key' => 'application/x-iwork-keynote-sffkey',
+        'key' => 'application/vnd.apple.keynote',
         'kfo' => 'application/vnd.kde.kformula',
         'kfo' => 'application/vnd.kde.kformula',
         'kia' => 'application/vnd.kidspiration',
         'kia' => 'application/vnd.kidspiration',
         'kml' => 'application/vnd.google-earth.kml+xml',
         'kml' => 'application/vnd.google-earth.kml+xml',
@@ -495,7 +519,7 @@ final class MimeType
         'les' => 'application/vnd.hhe.lesson-player',
         'les' => 'application/vnd.hhe.lesson-player',
         'less' => 'text/less',
         'less' => 'text/less',
         'lgr' => 'application/lgr+xml',
         'lgr' => 'application/lgr+xml',
-        'lha' => 'application/octet-stream',
+        'lha' => 'application/x-lzh-compressed',
         'link66' => 'application/vnd.route66.link66+xml',
         'link66' => 'application/vnd.route66.link66+xml',
         'list' => 'text/plain',
         'list' => 'text/plain',
         'list3820' => 'application/vnd.ibm.modcap',
         'list3820' => 'application/vnd.ibm.modcap',
@@ -504,6 +528,7 @@ final class MimeType
         'lnk' => 'application/x-ms-shortcut',
         'lnk' => 'application/x-ms-shortcut',
         'log' => 'text/plain',
         'log' => 'text/plain',
         'lostxml' => 'application/lost+xml',
         'lostxml' => 'application/lost+xml',
+        'lottie' => 'application/zip+dotlottie',
         'lrf' => 'application/octet-stream',
         'lrf' => 'application/octet-stream',
         'lrm' => 'application/vnd.ms-lrm',
         'lrm' => 'application/vnd.ms-lrm',
         'ltf' => 'application/vnd.frogans.ltf',
         'ltf' => 'application/vnd.frogans.ltf',
@@ -511,21 +536,24 @@ final class MimeType
         'luac' => 'application/x-lua-bytecode',
         'luac' => 'application/x-lua-bytecode',
         'lvp' => 'audio/vnd.lucent.voice',
         'lvp' => 'audio/vnd.lucent.voice',
         'lwp' => 'application/vnd.lotus-wordpro',
         'lwp' => 'application/vnd.lotus-wordpro',
-        'lzh' => 'application/octet-stream',
+        'lzh' => 'application/x-lzh-compressed',
+        'm13' => 'application/x-msmediaview',
+        'm14' => 'application/x-msmediaview',
         'm1v' => 'video/mpeg',
         'm1v' => 'video/mpeg',
+        'm21' => 'application/mp21',
         'm2a' => 'audio/mpeg',
         'm2a' => 'audio/mpeg',
+        'm2t' => 'video/mp2t',
+        'm2ts' => 'video/mp2t',
         'm2v' => 'video/mpeg',
         'm2v' => 'video/mpeg',
         'm3a' => 'audio/mpeg',
         'm3a' => 'audio/mpeg',
-        'm3u' => 'text/plain',
+        'm3u' => 'audio/x-mpegurl',
         'm3u8' => 'application/vnd.apple.mpegurl',
         'm3u8' => 'application/vnd.apple.mpegurl',
-        'm4a' => 'audio/x-m4a',
+        'm4a' => 'audio/mp4',
+        'm4b' => 'audio/mp4',
         'm4p' => 'application/mp4',
         'm4p' => 'application/mp4',
         'm4s' => 'video/iso.segment',
         'm4s' => 'video/iso.segment',
-        'm4u' => 'application/vnd.mpegurl',
+        'm4u' => 'video/vnd.mpegurl',
         'm4v' => 'video/x-m4v',
         'm4v' => 'video/x-m4v',
-        'm13' => 'application/x-msmediaview',
-        'm14' => 'application/x-msmediaview',
-        'm21' => 'application/mp21',
         'ma' => 'application/mathematica',
         'ma' => 'application/mathematica',
         'mads' => 'application/mads+xml',
         'mads' => 'application/mads+xml',
         'maei' => 'application/mmt-aei+xml',
         'maei' => 'application/mmt-aei+xml',
@@ -556,6 +584,8 @@ final class MimeType
         'mft' => 'application/rpki-manifest',
         'mft' => 'application/rpki-manifest',
         'mgp' => 'application/vnd.osgeo.mapguide.package',
         'mgp' => 'application/vnd.osgeo.mapguide.package',
         'mgz' => 'application/vnd.proteus.magazine',
         'mgz' => 'application/vnd.proteus.magazine',
+        'mht' => 'message/rfc822',
+        'mhtml' => 'message/rfc822',
         'mid' => 'audio/midi',
         'mid' => 'audio/midi',
         'midi' => 'audio/midi',
         'midi' => 'audio/midi',
         'mie' => 'application/x-mie',
         'mie' => 'application/x-mie',
@@ -564,11 +594,11 @@ final class MimeType
         'mj2' => 'video/mj2',
         'mj2' => 'video/mj2',
         'mjp2' => 'video/mj2',
         'mjp2' => 'video/mj2',
         'mjs' => 'text/javascript',
         'mjs' => 'text/javascript',
-        'mk3d' => 'video/x-matroska',
-        'mka' => 'audio/x-matroska',
+        'mk3d' => 'video/matroska-3d',
+        'mka' => 'audio/matroska',
         'mkd' => 'text/x-markdown',
         'mkd' => 'text/x-markdown',
         'mks' => 'video/x-matroska',
         'mks' => 'video/x-matroska',
-        'mkv' => 'video/x-matroska',
+        'mkv' => 'video/matroska',
         'mlp' => 'application/vnd.dolby.mlp',
         'mlp' => 'application/vnd.dolby.mlp',
         'mmd' => 'application/vnd.chipnuts.karaoke-mmd',
         'mmd' => 'application/vnd.chipnuts.karaoke-mmd',
         'mmf' => 'application/vnd.smaf',
         'mmf' => 'application/vnd.smaf',
@@ -581,13 +611,13 @@ final class MimeType
         'mov' => 'video/quicktime',
         'mov' => 'video/quicktime',
         'movie' => 'video/x-sgi-movie',
         'movie' => 'video/x-sgi-movie',
         'mp2' => 'audio/mpeg',
         'mp2' => 'audio/mpeg',
+        'mp21' => 'application/mp21',
         'mp2a' => 'audio/mpeg',
         'mp2a' => 'audio/mpeg',
         'mp3' => 'audio/mpeg',
         'mp3' => 'audio/mpeg',
         'mp4' => 'video/mp4',
         'mp4' => 'video/mp4',
         'mp4a' => 'audio/mp4',
         'mp4a' => 'audio/mp4',
         'mp4s' => 'application/mp4',
         'mp4s' => 'application/mp4',
         'mp4v' => 'video/mp4',
         'mp4v' => 'video/mp4',
-        'mp21' => 'application/mp21',
         'mpc' => 'application/vnd.mophun.certificate',
         'mpc' => 'application/vnd.mophun.certificate',
         'mpd' => 'application/dash+xml',
         'mpd' => 'application/dash+xml',
         'mpe' => 'video/mpeg',
         'mpe' => 'video/mpeg',
@@ -612,7 +642,7 @@ final class MimeType
         'msf' => 'application/vnd.epson.msf',
         'msf' => 'application/vnd.epson.msf',
         'msg' => 'application/vnd.ms-outlook',
         'msg' => 'application/vnd.ms-outlook',
         'msh' => 'model/mesh',
         'msh' => 'model/mesh',
-        'msi' => 'application/x-msdownload',
+        'msi' => 'application/octet-stream',
         'msix' => 'application/msix',
         'msix' => 'application/msix',
         'msixbundle' => 'application/msixbundle',
         'msixbundle' => 'application/msixbundle',
         'msl' => 'application/vnd.mobius.msl',
         'msl' => 'application/vnd.mobius.msl',
@@ -620,7 +650,7 @@ final class MimeType
         'msp' => 'application/octet-stream',
         'msp' => 'application/octet-stream',
         'msty' => 'application/vnd.muvee.style',
         'msty' => 'application/vnd.muvee.style',
         'mtl' => 'model/mtl',
         'mtl' => 'model/mtl',
-        'mts' => 'model/vnd.mts',
+        'mts' => 'video/mp2t',
         'mus' => 'application/vnd.musician',
         'mus' => 'application/vnd.musician',
         'musd' => 'application/mmt-usd+xml',
         'musd' => 'application/mmt-usd+xml',
         'musicxml' => 'application/vnd.recordare.musicxml+xml',
         'musicxml' => 'application/vnd.recordare.musicxml+xml',
@@ -639,6 +669,7 @@ final class MimeType
         'nbp' => 'application/vnd.wolfram.player',
         'nbp' => 'application/vnd.wolfram.player',
         'nc' => 'application/x-netcdf',
         'nc' => 'application/x-netcdf',
         'ncx' => 'application/x-dtbncx+xml',
         'ncx' => 'application/x-dtbncx+xml',
+        'ndjson' => 'application/x-ndjson',
         'nfo' => 'text/x-nfo',
         'nfo' => 'text/x-nfo',
         'ngdat' => 'application/vnd.nokia.n-gage.data',
         'ngdat' => 'application/vnd.nokia.n-gage.data',
         'nitf' => 'application/vnd.nitf',
         'nitf' => 'application/vnd.nitf',
@@ -653,7 +684,7 @@ final class MimeType
         'nsf' => 'application/vnd.lotus-notes',
         'nsf' => 'application/vnd.lotus-notes',
         'nt' => 'application/n-triples',
         'nt' => 'application/n-triples',
         'ntf' => 'application/vnd.nitf',
         'ntf' => 'application/vnd.nitf',
-        'numbers' => 'application/x-iwork-numbers-sffnumbers',
+        'numbers' => 'application/vnd.apple.numbers',
         'nzb' => 'application/x-nzb',
         'nzb' => 'application/x-nzb',
         'oa2' => 'application/vnd.fujitsu.oasys2',
         'oa2' => 'application/vnd.fujitsu.oasys2',
         'oa3' => 'application/vnd.fujitsu.oasys3',
         'oa3' => 'application/vnd.fujitsu.oasys3',
@@ -678,6 +709,8 @@ final class MimeType
         'ogv' => 'video/ogg',
         'ogv' => 'video/ogg',
         'ogx' => 'application/ogg',
         'ogx' => 'application/ogg',
         'omdoc' => 'application/omdoc+xml',
         'omdoc' => 'application/omdoc+xml',
+        'one' => 'application/onenote',
+        'onea' => 'application/onenote',
         'onepkg' => 'application/onenote',
         'onepkg' => 'application/onenote',
         'onetmp' => 'application/onenote',
         'onetmp' => 'application/onenote',
         'onetoc' => 'application/onenote',
         'onetoc' => 'application/onenote',
@@ -686,7 +719,7 @@ final class MimeType
         'opml' => 'text/x-opml',
         'opml' => 'text/x-opml',
         'oprc' => 'application/vnd.palm',
         'oprc' => 'application/vnd.palm',
         'opus' => 'audio/ogg',
         'opus' => 'audio/ogg',
-        'org' => 'text/x-org',
+        'org' => 'application/vnd.lotus-organizer',
         'osf' => 'application/vnd.yamaha.openscoreformat',
         'osf' => 'application/vnd.yamaha.openscoreformat',
         'osfpvg' => 'application/vnd.yamaha.openscoreformat.osfpvg+xml',
         'osfpvg' => 'application/vnd.yamaha.openscoreformat.osfpvg+xml',
         'osm' => 'application/vnd.openstreetmap.data+xml',
         'osm' => 'application/vnd.openstreetmap.data+xml',
@@ -704,17 +737,20 @@ final class MimeType
         'oxps' => 'application/oxps',
         'oxps' => 'application/oxps',
         'oxt' => 'application/vnd.openofficeorg.extension',
         'oxt' => 'application/vnd.openofficeorg.extension',
         'p' => 'text/x-pascal',
         'p' => 'text/x-pascal',
+        'p10' => 'application/pkcs10',
+        'p12' => 'application/x-pkcs12',
+        'p21' => 'model/step',
         'p7a' => 'application/x-pkcs7-signature',
         'p7a' => 'application/x-pkcs7-signature',
         'p7b' => 'application/x-pkcs7-certificates',
         'p7b' => 'application/x-pkcs7-certificates',
         'p7c' => 'application/pkcs7-mime',
         'p7c' => 'application/pkcs7-mime',
+        'p7e' => 'application/pkcs7-mime',
         'p7m' => 'application/pkcs7-mime',
         'p7m' => 'application/pkcs7-mime',
         'p7r' => 'application/x-pkcs7-certreqresp',
         'p7r' => 'application/x-pkcs7-certreqresp',
         'p7s' => 'application/pkcs7-signature',
         'p7s' => 'application/pkcs7-signature',
         'p8' => 'application/pkcs8',
         'p8' => 'application/pkcs8',
-        'p10' => 'application/x-pkcs10',
-        'p12' => 'application/x-pkcs12',
         'pac' => 'application/x-ns-proxy-autoconfig',
         'pac' => 'application/x-ns-proxy-autoconfig',
-        'pages' => 'application/x-iwork-pages-sffpages',
+        'pages' => 'application/vnd.apple.pages',
+        'parquet' => 'application/vnd.apache.parquet',
         'pas' => 'text/x-pascal',
         'pas' => 'text/x-pascal',
         'paw' => 'application/vnd.pawaafile',
         'paw' => 'application/vnd.pawaafile',
         'pbd' => 'application/vnd.powerbuilder6',
         'pbd' => 'application/vnd.powerbuilder6',
@@ -725,8 +761,8 @@ final class MimeType
         'pclxl' => 'application/vnd.hp-pclxl',
         'pclxl' => 'application/vnd.hp-pclxl',
         'pct' => 'image/x-pict',
         'pct' => 'image/x-pict',
         'pcurl' => 'application/vnd.curl.pcurl',
         'pcurl' => 'application/vnd.curl.pcurl',
-        'pcx' => 'image/x-pcx',
-        'pdb' => 'application/x-pilot',
+        'pcx' => 'image/vnd.zbrush.pcx',
+        'pdb' => 'application/vnd.palm',
         'pde' => 'text/x-processing',
         'pde' => 'text/x-processing',
         'pdf' => 'application/pdf',
         'pdf' => 'application/pdf',
         'pem' => 'application/x-x509-user-cert',
         'pem' => 'application/x-x509-user-cert',
@@ -737,7 +773,7 @@ final class MimeType
         'pfx' => 'application/x-pkcs12',
         'pfx' => 'application/x-pkcs12',
         'pgm' => 'image/x-portable-graymap',
         'pgm' => 'image/x-portable-graymap',
         'pgn' => 'application/x-chess-pgn',
         'pgn' => 'application/x-chess-pgn',
-        'pgp' => 'application/pgp',
+        'pgp' => 'application/pgp-encrypted',
         'phar' => 'application/octet-stream',
         'phar' => 'application/octet-stream',
         'php' => 'application/x-httpd-php',
         'php' => 'application/x-httpd-php',
         'php3' => 'application/x-httpd-php',
         'php3' => 'application/x-httpd-php',
@@ -760,17 +796,17 @@ final class MimeType
         'pnm' => 'image/x-portable-anymap',
         'pnm' => 'image/x-portable-anymap',
         'portpkg' => 'application/vnd.macports.portpkg',
         'portpkg' => 'application/vnd.macports.portpkg',
         'pot' => 'application/vnd.ms-powerpoint',
         'pot' => 'application/vnd.ms-powerpoint',
-        'potm' => 'application/vnd.ms-powerpoint.presentation.macroEnabled.12',
+        'potm' => 'application/vnd.ms-powerpoint.template.macroenabled.12',
         'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template',
         'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template',
         'ppa' => 'application/vnd.ms-powerpoint',
         'ppa' => 'application/vnd.ms-powerpoint',
-        'ppam' => 'application/vnd.ms-powerpoint.addin.macroEnabled.12',
+        'ppam' => 'application/vnd.ms-powerpoint.addin.macroenabled.12',
         'ppd' => 'application/vnd.cups-ppd',
         'ppd' => 'application/vnd.cups-ppd',
         'ppm' => 'image/x-portable-pixmap',
         'ppm' => 'image/x-portable-pixmap',
         'pps' => 'application/vnd.ms-powerpoint',
         'pps' => 'application/vnd.ms-powerpoint',
-        'ppsm' => 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12',
+        'ppsm' => 'application/vnd.ms-powerpoint.slideshow.macroenabled.12',
         'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
         'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
-        'ppt' => 'application/powerpoint',
-        'pptm' => 'application/vnd.ms-powerpoint.presentation.macroEnabled.12',
+        'ppt' => 'application/vnd.ms-powerpoint',
+        'pptm' => 'application/vnd.ms-powerpoint.presentation.macroenabled.12',
         'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
         'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
         'pqa' => 'application/vnd.palm',
         'pqa' => 'application/vnd.palm',
         'prc' => 'model/prc',
         'prc' => 'model/prc',
@@ -779,14 +815,16 @@ final class MimeType
         'provx' => 'application/provenance+xml',
         'provx' => 'application/provenance+xml',
         'ps' => 'application/postscript',
         'ps' => 'application/postscript',
         'psb' => 'application/vnd.3gpp.pic-bw-small',
         'psb' => 'application/vnd.3gpp.pic-bw-small',
-        'psd' => 'application/x-photoshop',
+        'psd' => 'image/vnd.adobe.photoshop',
         'psf' => 'application/x-font-linux-psf',
         'psf' => 'application/x-font-linux-psf',
         'pskcxml' => 'application/pskc+xml',
         'pskcxml' => 'application/pskc+xml',
         'pti' => 'image/prs.pti',
         'pti' => 'image/prs.pti',
         'ptid' => 'application/vnd.pvi.ptid1',
         'ptid' => 'application/vnd.pvi.ptid1',
         'pub' => 'application/x-mspublisher',
         'pub' => 'application/x-mspublisher',
+        'pv' => 'application/octet-stream',
         'pvb' => 'application/vnd.3gpp.pic-bw-var',
         'pvb' => 'application/vnd.3gpp.pic-bw-var',
         'pwn' => 'application/vnd.3m.post-it-notes',
         'pwn' => 'application/vnd.3m.post-it-notes',
+        'pxf' => 'application/octet-stream',
         'pya' => 'audio/vnd.ms-playready.media.pya',
         'pya' => 'audio/vnd.ms-playready.media.pya',
         'pyo' => 'model/vnd.pytha.pyox',
         'pyo' => 'model/vnd.pytha.pyox',
         'pyox' => 'model/vnd.pytha.pyox',
         'pyox' => 'model/vnd.pytha.pyox',
@@ -806,7 +844,7 @@ final class MimeType
         'ram' => 'audio/x-pn-realaudio',
         'ram' => 'audio/x-pn-realaudio',
         'raml' => 'application/raml+yaml',
         'raml' => 'application/raml+yaml',
         'rapd' => 'application/route-apd+xml',
         'rapd' => 'application/route-apd+xml',
-        'rar' => 'application/x-rar',
+        'rar' => 'application/vnd.rar',
         'ras' => 'image/x-cmu-raster',
         'ras' => 'image/x-cmu-raster',
         'rcprofile' => 'application/vnd.ipunplugged.rcprofile',
         'rcprofile' => 'application/vnd.ipunplugged.rcprofile',
         'rdf' => 'application/rdf+xml',
         'rdf' => 'application/rdf+xml',
@@ -821,7 +859,7 @@ final class MimeType
         'rl' => 'application/resource-lists+xml',
         'rl' => 'application/resource-lists+xml',
         'rlc' => 'image/vnd.fujixerox.edmics-rlc',
         'rlc' => 'image/vnd.fujixerox.edmics-rlc',
         'rld' => 'application/resource-lists-diff+xml',
         'rld' => 'application/resource-lists-diff+xml',
-        'rm' => 'audio/x-pn-realaudio',
+        'rm' => 'application/vnd.rn-realmedia',
         'rmi' => 'audio/midi',
         'rmi' => 'audio/midi',
         'rmp' => 'audio/x-pn-realaudio-plugin',
         'rmp' => 'audio/x-pn-realaudio-plugin',
         'rms' => 'application/vnd.jcp.javame.midlet-rms',
         'rms' => 'application/vnd.jcp.javame.midlet-rms',
@@ -831,7 +869,7 @@ final class MimeType
         'roa' => 'application/rpki-roa',
         'roa' => 'application/rpki-roa',
         'roff' => 'text/troff',
         'roff' => 'text/troff',
         'rp9' => 'application/vnd.cloanto.rp9',
         'rp9' => 'application/vnd.cloanto.rp9',
-        'rpm' => 'audio/x-pn-realaudio-plugin',
+        'rpm' => 'application/x-redhat-package-manager',
         'rpss' => 'application/vnd.nokia.radio-presets',
         'rpss' => 'application/vnd.nokia.radio-presets',
         'rpst' => 'application/vnd.nokia.radio-preset',
         'rpst' => 'application/vnd.nokia.radio-preset',
         'rq' => 'application/sparql-query',
         'rq' => 'application/sparql-query',
@@ -865,7 +903,7 @@ final class MimeType
         'sdkm' => 'application/vnd.solent.sdkm+xml',
         'sdkm' => 'application/vnd.solent.sdkm+xml',
         'sdp' => 'application/sdp',
         'sdp' => 'application/sdp',
         'sdw' => 'application/vnd.stardivision.writer',
         'sdw' => 'application/vnd.stardivision.writer',
-        'sea' => 'application/octet-stream',
+        'sea' => 'application/x-sea',
         'see' => 'application/vnd.seemail',
         'see' => 'application/vnd.seemail',
         'seed' => 'application/vnd.fdsn.seed',
         'seed' => 'application/vnd.fdsn.seed',
         'sema' => 'application/vnd.sema',
         'sema' => 'application/vnd.sema',
@@ -910,8 +948,8 @@ final class MimeType
         'slt' => 'application/vnd.epson.salt',
         'slt' => 'application/vnd.epson.salt',
         'sm' => 'application/vnd.stepmania.stepchart',
         'sm' => 'application/vnd.stepmania.stepchart',
         'smf' => 'application/vnd.stardivision.math',
         'smf' => 'application/vnd.stardivision.math',
-        'smi' => 'application/smil',
-        'smil' => 'application/smil',
+        'smi' => 'application/smil+xml',
+        'smil' => 'application/smil+xml',
         'smv' => 'video/x-smv',
         'smv' => 'video/x-smv',
         'smzip' => 'application/vnd.stepmania.package',
         'smzip' => 'application/vnd.stepmania.package',
         'snd' => 'audio/basic',
         'snd' => 'audio/basic',
@@ -925,7 +963,9 @@ final class MimeType
         'spp' => 'application/scvp-vp-response',
         'spp' => 'application/scvp-vp-response',
         'spq' => 'application/scvp-vp-request',
         'spq' => 'application/scvp-vp-request',
         'spx' => 'audio/ogg',
         'spx' => 'audio/ogg',
-        'sql' => 'application/x-sql',
+        'sql' => 'application/sql',
+        'sqlite' => 'application/vnd.sqlite3',
+        'sqlite3' => 'application/vnd.sqlite3',
         'src' => 'application/x-wais-source',
         'src' => 'application/x-wais-source',
         'srt' => 'application/x-subrip',
         'srt' => 'application/x-subrip',
         'sru' => 'application/sru+xml',
         'sru' => 'application/sru+xml',
@@ -938,12 +978,13 @@ final class MimeType
         'st' => 'application/vnd.sailingtracker.track',
         'st' => 'application/vnd.sailingtracker.track',
         'stc' => 'application/vnd.sun.xml.calc.template',
         'stc' => 'application/vnd.sun.xml.calc.template',
         'std' => 'application/vnd.sun.xml.draw.template',
         'std' => 'application/vnd.sun.xml.draw.template',
-        'step' => 'application/STEP',
+        'step' => 'model/step',
         'stf' => 'application/vnd.wt.stf',
         'stf' => 'application/vnd.wt.stf',
         'sti' => 'application/vnd.sun.xml.impress.template',
         'sti' => 'application/vnd.sun.xml.impress.template',
         'stk' => 'application/hyperstudio',
         'stk' => 'application/hyperstudio',
         'stl' => 'model/stl',
         'stl' => 'model/stl',
-        'stp' => 'application/STEP',
+        'stp' => 'model/step',
+        'stpnc' => 'model/step',
         'stpx' => 'model/step+xml',
         'stpx' => 'model/step+xml',
         'stpxz' => 'model/step-xml+zip',
         'stpxz' => 'model/step-xml+zip',
         'stpz' => 'model/step+zip',
         'stpz' => 'model/step+zip',
@@ -951,7 +992,7 @@ final class MimeType
         'stw' => 'application/vnd.sun.xml.writer.template',
         'stw' => 'application/vnd.sun.xml.writer.template',
         'styl' => 'text/stylus',
         'styl' => 'text/stylus',
         'stylus' => 'text/stylus',
         'stylus' => 'text/stylus',
-        'sub' => 'text/vnd.dvb.subtitle',
+        'sub' => 'image/vnd.dvb.subtitle',
         'sus' => 'application/vnd.sus-calendar',
         'sus' => 'application/vnd.sus-calendar',
         'susp' => 'application/vnd.sus-calendar',
         'susp' => 'application/vnd.sus-calendar',
         'sv4cpio' => 'application/x-sv4cpio',
         'sv4cpio' => 'application/x-sv4cpio',
@@ -970,6 +1011,7 @@ final class MimeType
         'sxi' => 'application/vnd.sun.xml.impress',
         'sxi' => 'application/vnd.sun.xml.impress',
         'sxm' => 'application/vnd.sun.xml.math',
         'sxm' => 'application/vnd.sun.xml.math',
         'sxw' => 'application/vnd.sun.xml.writer',
         'sxw' => 'application/vnd.sun.xml.writer',
+        'systemverify' => 'application/vnd.pp.systemverify+xml',
         't' => 'text/troff',
         't' => 'text/troff',
         't3' => 'application/x-t3vm-image',
         't3' => 'application/x-t3vm-image',
         't38' => 'image/t38',
         't38' => 'image/t38',
@@ -991,7 +1033,7 @@ final class MimeType
         'tfm' => 'application/x-tex-tfm',
         'tfm' => 'application/x-tex-tfm',
         'tfx' => 'image/tiff-fx',
         'tfx' => 'image/tiff-fx',
         'tga' => 'image/x-tga',
         'tga' => 'image/x-tga',
-        'tgz' => 'application/x-tar',
+        'tgz' => 'application/gzip',
         'thmx' => 'application/vnd.ms-officetheme',
         'thmx' => 'application/vnd.ms-officetheme',
         'tif' => 'image/tiff',
         'tif' => 'image/tiff',
         'tiff' => 'image/tiff',
         'tiff' => 'image/tiff',
@@ -1017,12 +1059,12 @@ final class MimeType
         'txd' => 'application/vnd.genomatix.tuxedo',
         'txd' => 'application/vnd.genomatix.tuxedo',
         'txf' => 'application/vnd.mobius.txf',
         'txf' => 'application/vnd.mobius.txf',
         'txt' => 'text/plain',
         'txt' => 'text/plain',
+        'u32' => 'application/x-authorware-bin',
         'u3d' => 'model/u3d',
         'u3d' => 'model/u3d',
         'u8dsn' => 'message/global-delivery-status',
         'u8dsn' => 'message/global-delivery-status',
         'u8hdr' => 'message/global-headers',
         'u8hdr' => 'message/global-headers',
         'u8mdn' => 'message/global-disposition-notification',
         'u8mdn' => 'message/global-disposition-notification',
         'u8msg' => 'message/global',
         'u8msg' => 'message/global',
-        'u32' => 'application/x-authorware-bin',
         'ubj' => 'application/ubjson',
         'ubj' => 'application/ubjson',
         'udeb' => 'application/x-debian-package',
         'udeb' => 'application/x-debian-package',
         'ufd' => 'application/vnd.ufdl',
         'ufd' => 'application/vnd.ufdl',
@@ -1078,16 +1120,18 @@ final class MimeType
         'vcx' => 'application/vnd.vcx',
         'vcx' => 'application/vnd.vcx',
         'vdi' => 'application/x-virtualbox-vdi',
         'vdi' => 'application/x-virtualbox-vdi',
         'vds' => 'model/vnd.sap.vds',
         'vds' => 'model/vnd.sap.vds',
+        'vdx' => 'application/vnd.ms-visio.viewer',
+        'vec' => 'application/vec+xml',
         'vhd' => 'application/x-virtualbox-vhd',
         'vhd' => 'application/x-virtualbox-vhd',
         'vis' => 'application/vnd.visionary',
         'vis' => 'application/vnd.visionary',
         'viv' => 'video/vnd.vivo',
         'viv' => 'video/vnd.vivo',
-        'vlc' => 'application/videolan',
         'vmdk' => 'application/x-virtualbox-vmdk',
         'vmdk' => 'application/x-virtualbox-vmdk',
         'vob' => 'video/x-ms-vob',
         'vob' => 'video/x-ms-vob',
         'vor' => 'application/vnd.stardivision.writer',
         'vor' => 'application/vnd.stardivision.writer',
         'vox' => 'application/x-authorware-bin',
         'vox' => 'application/x-authorware-bin',
         'vrml' => 'model/vrml',
         'vrml' => 'model/vrml',
         'vsd' => 'application/vnd.visio',
         'vsd' => 'application/vnd.visio',
+        'vsdx' => 'application/vnd.visio',
         'vsf' => 'application/vnd.vsf',
         'vsf' => 'application/vnd.vsf',
         'vss' => 'application/vnd.visio',
         'vss' => 'application/vnd.visio',
         'vst' => 'application/vnd.visio',
         'vst' => 'application/vnd.visio',
@@ -1095,17 +1139,18 @@ final class MimeType
         'vtf' => 'image/vnd.valve.source.texture',
         'vtf' => 'image/vnd.valve.source.texture',
         'vtt' => 'text/vtt',
         'vtt' => 'text/vtt',
         'vtu' => 'model/vnd.vtu',
         'vtu' => 'model/vnd.vtu',
+        'vtx' => 'application/vnd.visio',
         'vxml' => 'application/voicexml+xml',
         'vxml' => 'application/voicexml+xml',
         'w3d' => 'application/x-director',
         'w3d' => 'application/x-director',
         'wad' => 'application/x-doom',
         'wad' => 'application/x-doom',
         'wadl' => 'application/vnd.sun.wadl+xml',
         'wadl' => 'application/vnd.sun.wadl+xml',
         'war' => 'application/java-archive',
         'war' => 'application/java-archive',
         'wasm' => 'application/wasm',
         'wasm' => 'application/wasm',
-        'wav' => 'audio/x-wav',
+        'wav' => 'audio/wav',
         'wax' => 'audio/x-ms-wax',
         'wax' => 'audio/x-ms-wax',
         'wbmp' => 'image/vnd.wap.wbmp',
         'wbmp' => 'image/vnd.wap.wbmp',
         'wbs' => 'application/vnd.criticaltools.wbs+xml',
         'wbs' => 'application/vnd.criticaltools.wbs+xml',
-        'wbxml' => 'application/wbxml',
+        'wbxml' => 'application/vnd.wap.wbxml',
         'wcm' => 'application/vnd.ms-works',
         'wcm' => 'application/vnd.ms-works',
         'wdb' => 'application/vnd.ms-works',
         'wdb' => 'application/vnd.ms-works',
         'wdp' => 'image/vnd.ms-photo',
         'wdp' => 'image/vnd.ms-photo',
@@ -1124,12 +1169,12 @@ final class MimeType
         'wmd' => 'application/x-ms-wmd',
         'wmd' => 'application/x-ms-wmd',
         'wmf' => 'image/wmf',
         'wmf' => 'image/wmf',
         'wml' => 'text/vnd.wap.wml',
         'wml' => 'text/vnd.wap.wml',
-        'wmlc' => 'application/wmlc',
+        'wmlc' => 'application/vnd.wap.wmlc',
         'wmls' => 'text/vnd.wap.wmlscript',
         'wmls' => 'text/vnd.wap.wmlscript',
         'wmlsc' => 'application/vnd.wap.wmlscriptc',
         'wmlsc' => 'application/vnd.wap.wmlscriptc',
         'wmv' => 'video/x-ms-wmv',
         'wmv' => 'video/x-ms-wmv',
         'wmx' => 'video/x-ms-wmx',
         'wmx' => 'video/x-ms-wmx',
-        'wmz' => 'application/x-msmetafile',
+        'wmz' => 'application/x-ms-wmz',
         'woff' => 'font/woff',
         'woff' => 'font/woff',
         'woff2' => 'font/woff2',
         'woff2' => 'font/woff2',
         'word' => 'application/msword',
         'word' => 'application/msword',
@@ -1144,13 +1189,13 @@ final class MimeType
         'wspolicy' => 'application/wspolicy+xml',
         'wspolicy' => 'application/wspolicy+xml',
         'wtb' => 'application/vnd.webturbo',
         'wtb' => 'application/vnd.webturbo',
         'wvx' => 'video/x-ms-wvx',
         'wvx' => 'video/x-ms-wvx',
+        'x32' => 'application/x-authorware-bin',
         'x3d' => 'model/x3d+xml',
         'x3d' => 'model/x3d+xml',
         'x3db' => 'model/x3d+fastinfoset',
         'x3db' => 'model/x3d+fastinfoset',
         'x3dbz' => 'model/x3d+binary',
         'x3dbz' => 'model/x3d+binary',
         'x3dv' => 'model/x3d-vrml',
         'x3dv' => 'model/x3d-vrml',
         'x3dvz' => 'model/x3d+vrml',
         'x3dvz' => 'model/x3d+vrml',
         'x3dz' => 'model/x3d+xml',
         'x3dz' => 'model/x3d+xml',
-        'x32' => 'application/x-authorware-bin',
         'x_b' => 'model/vnd.parasolid.transmit.binary',
         'x_b' => 'model/vnd.parasolid.transmit.binary',
         'x_t' => 'model/vnd.parasolid.transmit.text',
         'x_t' => 'model/vnd.parasolid.transmit.text',
         'xaml' => 'application/xaml+xml',
         'xaml' => 'application/xaml+xml',
@@ -1162,6 +1207,7 @@ final class MimeType
         'xbm' => 'image/x-xbitmap',
         'xbm' => 'image/x-xbitmap',
         'xca' => 'application/xcap-caps+xml',
         'xca' => 'application/xcap-caps+xml',
         'xcs' => 'application/calendar+xml',
         'xcs' => 'application/calendar+xml',
+        'xdcf' => 'application/vnd.gov.sk.xmldatacontainer+xml',
         'xdf' => 'application/xcap-diff+xml',
         'xdf' => 'application/xcap-diff+xml',
         'xdm' => 'application/vnd.syncml.dm+xml',
         'xdm' => 'application/vnd.syncml.dm+xml',
         'xdp' => 'application/vnd.adobe.xdp+xml',
         'xdp' => 'application/vnd.adobe.xdp+xml',
@@ -1177,18 +1223,18 @@ final class MimeType
         'xhtml' => 'application/xhtml+xml',
         'xhtml' => 'application/xhtml+xml',
         'xhvml' => 'application/xv+xml',
         'xhvml' => 'application/xv+xml',
         'xif' => 'image/vnd.xiff',
         'xif' => 'image/vnd.xiff',
-        'xl' => 'application/excel',
+        'xl' => 'application/vnd.ms-excel',
         'xla' => 'application/vnd.ms-excel',
         'xla' => 'application/vnd.ms-excel',
-        'xlam' => 'application/vnd.ms-excel.addin.macroEnabled.12',
+        'xlam' => 'application/vnd.ms-excel.addin.macroenabled.12',
         'xlc' => 'application/vnd.ms-excel',
         'xlc' => 'application/vnd.ms-excel',
         'xlf' => 'application/xliff+xml',
         'xlf' => 'application/xliff+xml',
         'xlm' => 'application/vnd.ms-excel',
         'xlm' => 'application/vnd.ms-excel',
         'xls' => 'application/vnd.ms-excel',
         'xls' => 'application/vnd.ms-excel',
-        'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
-        'xlsm' => 'application/vnd.ms-excel.sheet.macroEnabled.12',
+        'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroenabled.12',
+        'xlsm' => 'application/vnd.ms-excel.sheet.macroenabled.12',
         'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
         'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
         'xlt' => 'application/vnd.ms-excel',
         'xlt' => 'application/vnd.ms-excel',
-        'xltm' => 'application/vnd.ms-excel.template.macroEnabled.12',
+        'xltm' => 'application/vnd.ms-excel.template.macroenabled.12',
         'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
         'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
         'xlw' => 'application/vnd.ms-excel',
         'xlw' => 'application/vnd.ms-excel',
         'xm' => 'audio/xm',
         'xm' => 'audio/xm',
@@ -1205,7 +1251,7 @@ final class MimeType
         'xpx' => 'application/vnd.intercon.formnet',
         'xpx' => 'application/vnd.intercon.formnet',
         'xsd' => 'application/xml',
         'xsd' => 'application/xml',
         'xsf' => 'application/prs.xsf+xml',
         'xsf' => 'application/prs.xsf+xml',
-        'xsl' => 'application/xml',
+        'xsl' => 'application/xslt+xml',
         'xslt' => 'application/xslt+xml',
         'xslt' => 'application/xslt+xml',
         'xsm' => 'application/vnd.syncml+xml',
         'xsm' => 'application/vnd.syncml+xml',
         'xspf' => 'application/xspf+xml',
         'xspf' => 'application/xspf+xml',
@@ -1254,6 +1300,6 @@ final class MimeType
      */
      */
     public static function fromExtension(string $extension): ?string
     public static function fromExtension(string $extension): ?string
     {
     {
-        return self::MIME_TYPES[strtolower($extension)] ?? null;
+        return self::MIME_TYPES[Utils::asciiToLower($extension)] ?? null;
     }
     }
 }
 }

+ 162 - 17
modules/boonex/xero/plugins/guzzlehttp/psr7/src/MultipartStream.php

@@ -20,20 +20,37 @@ final class MultipartStream implements StreamInterface
     /** @var StreamInterface */
     /** @var StreamInterface */
     private $stream;
     private $stream;
 
 
+    private const BOUNDARY_CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'()+_,-./:=? ";
+
     /**
     /**
-     * @param array  $elements Array of associative arrays, each containing a
-     *                         required "name" key mapping to the form field,
-     *                         name, a required "contents" key mapping to a
-     *                         StreamInterface/resource/string, an optional
-     *                         "headers" associative array of custom headers,
-     *                         and an optional "filename" key mapping to a
-     *                         string to send as the filename in the part.
-     * @param string $boundary You can optionally provide a specific boundary
+     * @param array       $elements Array of associative arrays, each containing a
+     *                              required "name" key mapping to the form field,
+     *                              name, a required "contents" key mapping to any
+     *                              non-array value accepted by Utils::streamFor()
+     *                              (non-string scalar field values are cast to
+     *                              string), or an array for nested expansion.
+     *                              Optional keys include "headers" (associative
+     *                              array of custom headers) and "filename" (string
+     *                              to send as the filename in the part).
+     *                              When "contents" is an array, it is recursively
+     *                              expanded into multiple fields using bracket notation
+     *                              (e.g., name[0][key]). Empty arrays produce no fields.
+     *                              The "filename" and "headers" options cannot be used
+     *                              with array contents.
+     * @param string|null $boundary You can optionally provide a specific boundary
      *
      *
      * @throws \InvalidArgumentException
      * @throws \InvalidArgumentException
      */
      */
     public function __construct(array $elements = [], ?string $boundary = null)
     public function __construct(array $elements = [], ?string $boundary = null)
     {
     {
+        if ($boundary !== null && !self::isValidBoundary($boundary)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing an invalid multipart boundary to MultipartStream::__construct() is deprecated; guzzlehttp/psr7 3.0 rejects invalid multipart boundaries.'
+            );
+        }
+
         $this->boundary = $boundary ?: bin2hex(random_bytes(20));
         $this->boundary = $boundary ?: bin2hex(random_bytes(20));
         $this->stream = $this->createStream($elements);
         $this->stream = $this->createStream($elements);
     }
     }
@@ -51,16 +68,17 @@ final class MultipartStream implements StreamInterface
     /**
     /**
      * Get the headers needed before transferring the content of a POST file
      * Get the headers needed before transferring the content of a POST file
      *
      *
-     * @param string[] $headers
+     * @param array<array-key, string> $headers
      */
      */
     private function getHeaders(array $headers): string
     private function getHeaders(array $headers): string
     {
     {
         $str = '';
         $str = '';
         foreach ($headers as $key => $value) {
         foreach ($headers as $key => $value) {
+            $key = (string) $key;
             $str .= "{$key}: {$value}\r\n";
             $str .= "{$key}: {$value}\r\n";
         }
         }
 
 
-        return "--{$this->boundary}\r\n".trim($str)."\r\n\r\n";
+        return "--{$this->boundary}\r\n".trim($str, " \n\r\t\0\x0B")."\r\n\r\n";
     }
     }
 
 
     /**
     /**
@@ -91,7 +109,43 @@ final class MultipartStream implements StreamInterface
             }
             }
         }
         }
 
 
-        $element['contents'] = Utils::streamFor($element['contents']);
+        if (!is_string($element['name']) && !is_int($element['name'])) {
+            throw new \InvalidArgumentException("The 'name' key must be a string or integer");
+        }
+
+        if (is_array($element['contents'])) {
+            if (array_key_exists('filename', $element) || array_key_exists('headers', $element)) {
+                throw new \InvalidArgumentException(
+                    "The 'filename' and 'headers' options cannot be used when 'contents' is an array"
+                );
+            }
+
+            $this->addNestedElements($stream, $element['contents'], (string) $element['name']);
+
+            return;
+        }
+
+        $contents = $element['contents'];
+        if (is_scalar($contents) && !is_string($contents)) {
+            // Multipart field values are byte strings on the wire, so finite
+            // numeric and boolean field values are cast to string here rather
+            // than tripping streamFor()'s non-string-scalar deprecation. Non-finite
+            // floats are deprecated and normalized here too, so the deprecation is
+            // reported against MultipartStream instead of transitively through
+            // streamFor().
+            if (is_float($contents) && !is_finite($contents)) {
+                \trigger_deprecation(
+                    'guzzlehttp/psr7',
+                    '2.12',
+                    'Passing a non-finite float as multipart contents is deprecated; guzzlehttp/psr7 3.0 rejects non-finite floats.'
+                );
+
+                $contents = is_nan($contents) ? 'NAN' : ($contents > 0 ? 'INF' : '-INF');
+            }
+
+            $contents = (string) $contents;
+        }
+        $element['contents'] = Utils::streamFor($contents);
 
 
         if (empty($element['filename'])) {
         if (empty($element['filename'])) {
             $uri = $element['contents']->getMetadata('uri');
             $uri = $element['contents']->getMetadata('uri');
@@ -101,7 +155,7 @@ final class MultipartStream implements StreamInterface
         }
         }
 
 
         [$body, $headers] = $this->createElement(
         [$body, $headers] = $this->createElement(
-            $element['name'],
+            (string) $element['name'],
             $element['contents'],
             $element['contents'],
             $element['filename'] ?? null,
             $element['filename'] ?? null,
             $element['headers'] ?? []
             $element['headers'] ?? []
@@ -113,12 +167,32 @@ final class MultipartStream implements StreamInterface
     }
     }
 
 
     /**
     /**
-     * @param string[] $headers
+     * Recursively expand array contents into multiple form fields.
      *
      *
-     * @return array{0: StreamInterface, 1: string[]}
+     * @param array<array-key, mixed> $contents
+     */
+    private function addNestedElements(AppendStream $stream, array $contents, string $root): void
+    {
+        foreach ($contents as $key => $value) {
+            $fieldName = $root === '' ? sprintf('[%s]', (string) $key) : sprintf('%s[%s]', $root, (string) $key);
+
+            if (is_array($value)) {
+                $this->addNestedElements($stream, $value, $fieldName);
+            } else {
+                $this->addElement($stream, ['name' => $fieldName, 'contents' => $value]);
+            }
+        }
+    }
+
+    /**
+     * @param array<array-key, mixed> $headers
+     *
+     * @return array{0: StreamInterface, 1: array<array-key, string>}
      */
      */
     private function createElement(string $name, StreamInterface $stream, ?string $filename, array $headers): array
     private function createElement(string $name, StreamInterface $stream, ?string $filename, array $headers): array
     {
     {
+        $headers = self::normalizePartHeaders($headers);
+
         // Set a default content-disposition header if one was no provided
         // Set a default content-disposition header if one was no provided
         $disposition = self::getHeader($headers, 'content-disposition');
         $disposition = self::getHeader($headers, 'content-disposition');
         if (!$disposition) {
         if (!$disposition) {
@@ -149,17 +223,88 @@ final class MultipartStream implements StreamInterface
     }
     }
 
 
     /**
     /**
-     * @param string[] $headers
+     * @param array<array-key, string> $headers
      */
      */
     private static function getHeader(array $headers, string $key): ?string
     private static function getHeader(array $headers, string $key): ?string
     {
     {
-        $lowercaseHeader = strtolower($key);
+        $lowercaseHeader = Utils::asciiToLower($key);
         foreach ($headers as $k => $v) {
         foreach ($headers as $k => $v) {
-            if (strtolower((string) $k) === $lowercaseHeader) {
+            if (Utils::asciiToLower((string) $k) === $lowercaseHeader) {
                 return $v;
                 return $v;
             }
             }
         }
         }
 
 
         return null;
         return null;
     }
     }
+
+    private static function isValidBoundary(string $boundary): bool
+    {
+        $length = strlen($boundary);
+
+        if ($length < 1 || $length > 70 || $boundary[$length - 1] === ' ') {
+            return false;
+        }
+
+        return strspn($boundary, self::BOUNDARY_CHARS) === $length;
+    }
+
+    /**
+     * @param array<array-key, mixed> $headers
+     *
+     * @return array<array-key, string>
+     */
+    private static function normalizePartHeaders(array $headers): array
+    {
+        $normalized = [];
+
+        foreach ($headers as $key => $value) {
+            self::deprecateInvalidPartHeaderName((string) $key);
+
+            if (!is_string($value)) {
+                if (!is_scalar($value) && $value !== null && !(is_object($value) && method_exists($value, '__toString'))) {
+                    throw new \InvalidArgumentException(sprintf(
+                        'Multipart part header value must be a string or stringable value but %s provided.',
+                        \get_debug_type($value)
+                    ));
+                }
+
+                \trigger_deprecation(
+                    'guzzlehttp/psr7',
+                    '2.11',
+                    'Passing %s as a multipart part header value is deprecated; guzzlehttp/psr7 3.0 requires string multipart part header values.',
+                    \get_debug_type($value)
+                );
+            }
+
+            $value = (string) $value;
+
+            self::deprecateInvalidPartHeaderValue($value);
+
+            $normalized[$key] = $value;
+        }
+
+        return $normalized;
+    }
+
+    private static function deprecateInvalidPartHeaderName(string $name): void
+    {
+        if (!preg_match('/^[a-zA-Z0-9\'`#$%&*+.^_|~!-]+$/D', $name)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing an invalid multipart part header name to MultipartStream is deprecated; guzzlehttp/psr7 3.0 rejects invalid multipart part header names.'
+            );
+        }
+    }
+
+    private static function deprecateInvalidPartHeaderValue(string $value): void
+    {
+        if (!preg_match('/^[\x20\x09\x21-\x7E\x80-\xFF]*$/D', $value)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing an invalid multipart part header value to MultipartStream is deprecated; guzzlehttp/psr7 3.0 rejects invalid multipart part header values.'
+            );
+        }
+    }
 }
 }

+ 18 - 0
modules/boonex/xero/plugins/guzzlehttp/psr7/src/NoSeekStream.php

@@ -18,6 +18,24 @@ final class NoSeekStream implements StreamInterface
 
 
     public function seek($offset, $whence = SEEK_SET): void
     public function seek($offset, $whence = SEEK_SET): void
     {
     {
+        if (!\is_int($offset)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $offset.',
+                \get_debug_type($offset)
+            );
+        }
+
+        if (!\is_int($whence)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $whence.',
+                \get_debug_type($whence)
+            );
+        }
+
         throw new \RuntimeException('Cannot seek a NoSeekStream');
         throw new \RuntimeException('Cannot seek a NoSeekStream');
     }
     }
 
 

+ 65 - 14
modules/boonex/xero/plugins/guzzlehttp/psr7/src/PumpStream.php

@@ -9,16 +9,19 @@ use Psr\Http\Message\StreamInterface;
 /**
 /**
  * Provides a read only stream that pumps data from a PHP callable.
  * Provides a read only stream that pumps data from a PHP callable.
  *
  *
- * When invoking the provided callable, the PumpStream will pass the amount of
- * data requested to read to the callable. The callable can choose to ignore
+ * When invoking the provided callable, the PumpStream will pass the suggested
+ * number of bytes to read to the callable. The callable can choose to ignore
  * this value and return fewer or more bytes than requested. Any extra data
  * this value and return fewer or more bytes than requested. Any extra data
- * returned by the provided callable is buffered internally until drained using
- * the read() function of the PumpStream. The provided callable MUST return
- * false when there is no more data to read.
+ * returned by the callable is buffered internally until drained using the
+ * read() function of the PumpStream. The callable MUST return false or null
+ * when there is no more data to read.
+ *
+ * Userland callables that declare no parameters are tolerated by PHP, but
+ * length-aware callables remain the recommended formal shape.
  */
  */
 final class PumpStream implements StreamInterface
 final class PumpStream implements StreamInterface
 {
 {
-    /** @var callable(int): (string|false|null)|null */
+    /** @var callable|null */
     private $source;
     private $source;
 
 
     /** @var int|null */
     /** @var int|null */
@@ -34,14 +37,17 @@ final class PumpStream implements StreamInterface
     private $buffer;
     private $buffer;
 
 
     /**
     /**
-     * @param callable(int): (string|false|null)  $source  Source of the stream data. The callable MAY
-     *                                                     accept an integer argument used to control the
-     *                                                     amount of data to return. The callable MUST
-     *                                                     return a string when called, or false|null on error
-     *                                                     or EOF.
-     * @param array{size?: int, metadata?: array} $options Stream options:
-     *                                                     - metadata: Hash of metadata to use with stream.
-     *                                                     - size: Size of the stream, if known.
+     * @param (callable(): (string|false|null))|(callable(int): (string|false|null)) $source  Source of the stream data. The callable receives
+     *                                                                                        the suggested number of bytes to read, may ignore
+     *                                                                                        that value, and may return fewer or more bytes.
+     *                                                                                        Extra bytes are buffered. The callable MUST return
+     *                                                                                        a string when called, or false|null on error or EOF.
+     *                                                                                        Userland callables that declare no parameters are
+     *                                                                                        tolerated by PHP, but length-aware callables remain
+     *                                                                                        the recommended formal shape.
+     * @param array{size?: int, metadata?: array}                                    $options Stream options:
+     *                                                                                        - metadata: Hash of metadata to use with stream.
+     *                                                                                        - size: Size of the stream, if known.
      */
      */
     public function __construct(callable $source, array $options = [])
     public function __construct(callable $source, array $options = [])
     {
     {
@@ -105,6 +111,24 @@ final class PumpStream implements StreamInterface
 
 
     public function seek($offset, $whence = SEEK_SET): void
     public function seek($offset, $whence = SEEK_SET): void
     {
     {
+        if (!\is_int($offset)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $offset.',
+                \get_debug_type($offset)
+            );
+        }
+
+        if (!\is_int($whence)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $whence.',
+                \get_debug_type($whence)
+            );
+        }
+
         throw new \RuntimeException('Cannot seek a PumpStream');
         throw new \RuntimeException('Cannot seek a PumpStream');
     }
     }
 
 
@@ -115,6 +139,15 @@ final class PumpStream implements StreamInterface
 
 
     public function write($string): int
     public function write($string): int
     {
     {
+        if (!\is_string($string)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing %s to StreamInterface::write() is deprecated; guzzlehttp/psr7 3.0 requires string for $string.',
+                \get_debug_type($string)
+            );
+        }
+
         throw new \RuntimeException('Cannot write to a PumpStream');
         throw new \RuntimeException('Cannot write to a PumpStream');
     }
     }
 
 
@@ -125,6 +158,15 @@ final class PumpStream implements StreamInterface
 
 
     public function read($length): string
     public function read($length): string
     {
     {
+        if (!\is_int($length)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing %s to StreamInterface::read() is deprecated; guzzlehttp/psr7 3.0 requires int for $length.',
+                \get_debug_type($length)
+            );
+        }
+
         $data = $this->buffer->read($length);
         $data = $this->buffer->read($length);
         $readLen = strlen($data);
         $readLen = strlen($data);
         $this->tellPos += $readLen;
         $this->tellPos += $readLen;
@@ -154,6 +196,15 @@ final class PumpStream implements StreamInterface
      */
      */
     public function getMetadata($key = null)
     public function getMetadata($key = null)
     {
     {
+        if ($key !== null && !\is_string($key)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing %s to StreamInterface::getMetadata() is deprecated; guzzlehttp/psr7 3.0 requires string|null for $key.',
+                \get_debug_type($key)
+            );
+        }
+
         if (!$key) {
         if (!$key) {
             return $this->metadata;
             return $this->metadata;
         }
         }

+ 25 - 2
modules/boonex/xero/plugins/guzzlehttp/psr7/src/Query.php

@@ -96,7 +96,7 @@ final class Query
             $k = $encoder((string) $k);
             $k = $encoder((string) $k);
             if (!is_array($v)) {
             if (!is_array($v)) {
                 $qs .= $k;
                 $qs .= $k;
-                $v = is_bool($v) ? $castBool($v) : $v;
+                $v = is_bool($v) ? $castBool($v) : self::normalizeNonFiniteFloat($v);
                 if ($v !== null) {
                 if ($v !== null) {
                     $qs .= '='.$encoder((string) $v);
                     $qs .= '='.$encoder((string) $v);
                 }
                 }
@@ -104,7 +104,7 @@ final class Query
             } else {
             } else {
                 foreach ($v as $vv) {
                 foreach ($v as $vv) {
                     $qs .= $k;
                     $qs .= $k;
-                    $vv = is_bool($vv) ? $castBool($vv) : $vv;
+                    $vv = is_bool($vv) ? $castBool($vv) : self::normalizeNonFiniteFloat($vv);
                     if ($vv !== null) {
                     if ($vv !== null) {
                         $qs .= '='.$encoder((string) $vv);
                         $qs .= '='.$encoder((string) $vv);
                     }
                     }
@@ -115,4 +115,27 @@ final class Query
 
 
         return $qs ? (string) substr($qs, 0, -1) : '';
         return $qs ? (string) substr($qs, 0, -1) : '';
     }
     }
+
+    /**
+     * Converts non-finite floats to the strings PHP coerces them to, as
+     * implicit coercion of NAN emits a warning on PHP 8.5.
+     *
+     * @param mixed $value
+     *
+     * @return mixed
+     */
+    private static function normalizeNonFiniteFloat($value)
+    {
+        if (is_float($value) && !is_finite($value)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.12',
+                'Passing a non-finite float to Query::build() is deprecated; guzzlehttp/psr7 3.0 rejects non-finite floats.'
+            );
+
+            return is_nan($value) ? 'NAN' : ($value > 0 ? 'INF' : '-INF');
+        }
+
+        return $value;
+    }
 }
 }

+ 40 - 4
modules/boonex/xero/plugins/guzzlehttp/psr7/src/Request.php

@@ -40,11 +40,14 @@ class Request implements RequestInterface
         string $version = '1.1'
         string $version = '1.1'
     ) {
     ) {
         $this->assertMethod($method);
         $this->assertMethod($method);
-        if (!($uri instanceof UriInterface)) {
+        $this->assertProtocolVersion($version);
+
+        if (!$uri instanceof UriInterface) {
             $uri = new Uri($uri);
             $uri = new Uri($uri);
         }
         }
 
 
-        $this->method = strtoupper($method);
+        self::warnOnMethodCasingChange($method);
+        $this->method = Utils::asciiToUpper($method);
         $this->uri = $uri;
         $this->uri = $uri;
         $this->setHeaders($headers);
         $this->setHeaders($headers);
         $this->protocol = $version;
         $this->protocol = $version;
@@ -77,7 +80,13 @@ class Request implements RequestInterface
 
 
     public function withRequestTarget($requestTarget): RequestInterface
     public function withRequestTarget($requestTarget): RequestInterface
     {
     {
-        if (preg_match('#\s#', $requestTarget)) {
+        $hasWhitespace = preg_match('#\s#', $requestTarget);
+
+        if ($hasWhitespace === false) {
+            throw new \RuntimeException('Unable to validate request target: '.preg_last_error_msg());
+        }
+
+        if ($hasWhitespace === 1) {
             throw new InvalidArgumentException(
             throw new InvalidArgumentException(
                 'Invalid request target provided; cannot contain whitespace'
                 'Invalid request target provided; cannot contain whitespace'
             );
             );
@@ -97,8 +106,9 @@ class Request implements RequestInterface
     public function withMethod($method): RequestInterface
     public function withMethod($method): RequestInterface
     {
     {
         $this->assertMethod($method);
         $this->assertMethod($method);
+        self::warnOnMethodCasingChange($method);
         $new = clone $this;
         $new = clone $this;
-        $new->method = strtoupper($method);
+        $new->method = Utils::asciiToUpper($method);
 
 
         return $new;
         return $new;
     }
     }
@@ -110,6 +120,15 @@ class Request implements RequestInterface
 
 
     public function withUri(UriInterface $uri, $preserveHost = false): RequestInterface
     public function withUri(UriInterface $uri, $preserveHost = false): RequestInterface
     {
     {
+        if (!\is_bool($preserveHost)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing %s to RequestInterface::withUri() is deprecated; guzzlehttp/psr7 3.0 requires bool for $preserveHost.',
+                \get_debug_type($preserveHost)
+            );
+        }
+
         if ($uri === $this->uri) {
         if ($uri === $this->uri) {
             return $this;
             return $this;
         }
         }
@@ -132,10 +151,14 @@ class Request implements RequestInterface
             return;
             return;
         }
         }
 
 
+        Uri::assertValidHost($host);
+
         if (($port = $this->uri->getPort()) !== null) {
         if (($port = $this->uri->getPort()) !== null) {
             $host .= ':'.$port;
             $host .= ':'.$port;
         }
         }
 
 
+        $this->assertValue($host);
+
         if (isset($this->headerNames['host'])) {
         if (isset($this->headerNames['host'])) {
             $header = $this->headerNames['host'];
             $header = $this->headerNames['host'];
         } else {
         } else {
@@ -155,5 +178,18 @@ class Request implements RequestInterface
         if (!is_string($method) || $method === '') {
         if (!is_string($method) || $method === '') {
             throw new InvalidArgumentException('Method must be a non-empty string.');
             throw new InvalidArgumentException('Method must be a non-empty string.');
         }
         }
+
+        $this->assertNoLineSeparators($method, 'Method');
+    }
+
+    private static function warnOnMethodCasingChange(string $method): void
+    {
+        if ($method !== Utils::asciiToUpper($method)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing a non-uppercase HTTP method is deprecated; guzzlehttp/psr7 3.0 preserves method casing and will no longer uppercase it. Normalize the method before constructing or modifying requests if uppercase is required.'
+            );
+        }
     }
     }
 }
 }

+ 27 - 3
modules/boonex/xero/plugins/guzzlehttp/psr7/src/Response.php

@@ -99,6 +99,7 @@ class Response implements ResponseInterface
         ?string $reason = null
         ?string $reason = null
     ) {
     ) {
         $this->assertStatusCodeRange($status);
         $this->assertStatusCodeRange($status);
+        $this->assertProtocolVersion($version);
 
 
         $this->statusCode = $status;
         $this->statusCode = $status;
 
 
@@ -108,11 +109,14 @@ class Response implements ResponseInterface
 
 
         $this->setHeaders($headers);
         $this->setHeaders($headers);
         if ($reason == '' && isset(self::PHRASES[$this->statusCode])) {
         if ($reason == '' && isset(self::PHRASES[$this->statusCode])) {
-            $this->reasonPhrase = self::PHRASES[$this->statusCode];
+            $reasonPhrase = self::PHRASES[$this->statusCode];
         } else {
         } else {
-            $this->reasonPhrase = (string) $reason;
+            $reasonPhrase = (string) $reason;
         }
         }
 
 
+        $this->assertNoLineSeparators($reasonPhrase, 'Reason phrase');
+        $this->reasonPhrase = $reasonPhrase;
+
         $this->protocol = $version;
         $this->protocol = $version;
     }
     }
 
 
@@ -128,6 +132,24 @@ class Response implements ResponseInterface
 
 
     public function withStatus($code, $reasonPhrase = ''): ResponseInterface
     public function withStatus($code, $reasonPhrase = ''): ResponseInterface
     {
     {
+        if (!\is_int($code) && \filter_var($code, \FILTER_VALIDATE_INT) !== false) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing %s to ResponseInterface::withStatus() is deprecated; guzzlehttp/psr7 3.0 requires int for $code.',
+                \get_debug_type($code)
+            );
+        }
+
+        if (!\is_string($reasonPhrase)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing %s to ResponseInterface::withStatus() is deprecated; guzzlehttp/psr7 3.0 requires string for $reasonPhrase.',
+                \get_debug_type($reasonPhrase)
+            );
+        }
+
         $this->assertStatusCodeIsInteger($code);
         $this->assertStatusCodeIsInteger($code);
         $code = (int) $code;
         $code = (int) $code;
         $this->assertStatusCodeRange($code);
         $this->assertStatusCodeRange($code);
@@ -137,7 +159,9 @@ class Response implements ResponseInterface
         if ($reasonPhrase == '' && isset(self::PHRASES[$new->statusCode])) {
         if ($reasonPhrase == '' && isset(self::PHRASES[$new->statusCode])) {
             $reasonPhrase = self::PHRASES[$new->statusCode];
             $reasonPhrase = self::PHRASES[$new->statusCode];
         }
         }
-        $new->reasonPhrase = (string) $reasonPhrase;
+        $reasonPhrase = (string) $reasonPhrase;
+        $this->assertNoLineSeparators($reasonPhrase, 'Reason phrase');
+        $new->reasonPhrase = $reasonPhrase;
 
 
         return $new;
         return $new;
     }
     }

+ 25 - 0
modules/boonex/xero/plugins/guzzlehttp/psr7/src/Rfc3986.php

@@ -0,0 +1,25 @@
+<?php
+
+declare(strict_types=1);
+
+namespace GuzzleHttp\Psr7;
+
+/**
+ * @internal
+ */
+final class Rfc3986
+{
+    /**
+     * Sub-delims for use in a regex.
+     *
+     * @see https://datatracker.ietf.org/doc/html/rfc3986#section-2.2
+     */
+    public const CHAR_SUB_DELIMS = '!\$&\'\(\)\*\+,;=';
+
+    /**
+     * Unreserved characters for use in a regex.
+     *
+     * @see https://datatracker.ietf.org/doc/html/rfc3986#section-2.3
+     */
+    public const CHAR_UNRESERVED = 'a-zA-Z0-9_\-\.~';
+}

+ 89 - 0
modules/boonex/xero/plugins/guzzlehttp/psr7/src/Rfc7230.php

@@ -20,4 +20,93 @@ final class Rfc7230
      */
      */
     public const HEADER_REGEX = "(^([^()<>@,;:\\\"/[\]?={}\x01-\x20\x7F]++):[ \t]*+((?:[ \t]*+[\x21-\x7E\x80-\xFF]++)*+)[ \t]*+\r?\n)m";
     public const HEADER_REGEX = "(^([^()<>@,;:\\\"/[\]?={}\x01-\x20\x7F]++):[ \t]*+((?:[ \t]*+[\x21-\x7E\x80-\xFF]++)*+)[ \t]*+\r?\n)m";
     public const HEADER_FOLD_REGEX = "(\r?\n[ \t]++)";
     public const HEADER_FOLD_REGEX = "(\r?\n[ \t]++)";
+
+    /**
+     * @return array{0: string, 1: int|null}|null
+     */
+    public static function parseHostHeader(string $authority): ?array
+    {
+        if ($authority === '') {
+            return null;
+        }
+
+        $host = $authority;
+        $port = null;
+
+        if ($authority[0] === '[') {
+            $closingBracket = strpos($authority, ']');
+            if ($closingBracket === false) {
+                return null;
+            }
+
+            $host = substr($authority, 0, $closingBracket + 1);
+            $remainder = substr($authority, $closingBracket + 1);
+            if ($remainder !== '') {
+                if ($remainder[0] !== ':') {
+                    return null;
+                }
+
+                $port = self::parseAuthorityPort(substr($remainder, 1));
+                if ($port === null) {
+                    return null;
+                }
+            }
+        } elseif (false !== ($colon = strpos($authority, ':'))) {
+            $host = substr($authority, 0, $colon);
+            $port = self::parseAuthorityPort(substr($authority, $colon + 1));
+            if ($port === null) {
+                return null;
+            }
+        }
+
+        if ($host === '' || !self::isValidHostHeaderHost($host)) {
+            return null;
+        }
+
+        return [$host, $port];
+    }
+
+    private static function isValidHostHeaderHost(string $host): bool
+    {
+        $invalidHost = preg_match('/[\x00-\x20\x7F\/\?#@\\\\]/', $host);
+
+        if ($invalidHost === false) {
+            return false;
+        }
+
+        if ($invalidHost === 1) {
+            return false;
+        }
+
+        if (strpos($host, '[') !== false || strpos($host, ']') !== false) {
+            if ($host[0] !== '[' || substr($host, -1) !== ']') {
+                return false;
+            }
+
+            $address = substr($host, 1, -1);
+
+            return filter_var($address, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6) !== false
+                || preg_match('/^v[0-9a-f]+\.['.Rfc3986::CHAR_UNRESERVED.Rfc3986::CHAR_SUB_DELIMS.':]+$/iD', $address) === 1;
+        }
+
+        return strpos($host, ':') === false;
+    }
+
+    private static function parseAuthorityPort(string $port): ?int
+    {
+        if ($port === '' || !ctype_digit($port)) {
+            return null;
+        }
+
+        $normalized = ltrim($port, '0');
+        if ($normalized === '') {
+            return 0;
+        }
+
+        if (strlen($normalized) > 5 || (int) $normalized > 0xFFFF) {
+            return null;
+        }
+
+        return (int) $normalized;
+    }
 }
 }

+ 142 - 24
modules/boonex/xero/plugins/guzzlehttp/psr7/src/ServerRequest.php

@@ -165,11 +165,12 @@ class ServerRequest extends Request implements ServerRequestInterface
      */
      */
     public static function fromGlobals(): ServerRequestInterface
     public static function fromGlobals(): ServerRequestInterface
     {
     {
-        $method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
-        $headers = getallheaders();
+        $method = Utils::asciiToUpper(self::getServerParam('REQUEST_METHOD') ?? 'GET');
+        $headers = self::removeInvalidHostHeader(self::getAllHeaders());
         $uri = self::getUriFromGlobals();
         $uri = self::getUriFromGlobals();
         $body = new CachingStream(new LazyOpenStream('php://input', 'r+'));
         $body = new CachingStream(new LazyOpenStream('php://input', 'r+'));
-        $protocol = isset($_SERVER['SERVER_PROTOCOL']) ? str_replace('HTTP/', '', $_SERVER['SERVER_PROTOCOL']) : '1.1';
+        $serverProtocol = self::getServerParam('SERVER_PROTOCOL');
+        $protocol = $serverProtocol !== null ? str_replace('HTTP/', '', $serverProtocol) : '1.1';
 
 
         $serverRequest = new ServerRequest($method, $uri, $headers, $body, $protocol, $_SERVER);
         $serverRequest = new ServerRequest($method, $uri, $headers, $body, $protocol, $_SERVER);
 
 
@@ -180,18 +181,63 @@ class ServerRequest extends Request implements ServerRequestInterface
             ->withUploadedFiles(self::normalizeFiles($_FILES));
             ->withUploadedFiles(self::normalizeFiles($_FILES));
     }
     }
 
 
-    private static function extractHostAndPortFromAuthority(string $authority): array
+    /**
+     * @return array<array-key, string>
+     */
+    private static function getAllHeaders(): array
     {
     {
-        $uri = 'http://'.$authority;
-        $parts = parse_url($uri);
-        if (false === $parts) {
-            return [null, null];
+        return self::normalizeHeaderValues(getallheaders());
+    }
+
+    /**
+     * @param array<array-key, mixed> $headers
+     *
+     * @return array<array-key, string>
+     */
+    private static function normalizeHeaderValues(array $headers): array
+    {
+        $normalized = [];
+
+        foreach ($headers as $name => $value) {
+            if (is_scalar($value) || (is_object($value) && method_exists($value, '__toString'))) {
+                $normalized[$name] = (string) $value;
+            }
+        }
+
+        return $normalized;
+    }
+
+    private static function getServerParam(string $key): ?string
+    {
+        return isset($_SERVER[$key]) && is_string($_SERVER[$key]) ? $_SERVER[$key] : null;
+    }
+
+    /**
+     * @param array<array-key, string> $headers
+     *
+     * @return array<array-key, string>
+     */
+    private static function removeInvalidHostHeader(array $headers): array
+    {
+        foreach ($headers as $name => $value) {
+            if (Utils::asciiToLower((string) $name) !== 'host') {
+                continue;
+            }
+
+            if (Rfc7230::parseHostHeader($value) === null) {
+                unset($headers[$name]);
+            }
         }
         }
 
 
-        $host = $parts['host'] ?? null;
-        $port = $parts['port'] ?? null;
+        return $headers;
+    }
 
 
-        return [$host, $port];
+    /**
+     * @return array{0: string|null, 1: int|null}
+     */
+    private static function extractHostAndPortFromAuthority(string $authority): array
+    {
+        return Rfc7230::parseHostHeader($authority) ?? [null, null];
     }
     }
 
 
     /**
     /**
@@ -201,11 +247,13 @@ class ServerRequest extends Request implements ServerRequestInterface
     {
     {
         $uri = new Uri('');
         $uri = new Uri('');
 
 
-        $uri = $uri->withScheme(!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' ? 'https' : 'http');
+        $https = self::getServerParam('HTTPS');
+        $uri = $uri->withScheme(!empty($https) && $https !== 'off' ? 'https' : 'http');
 
 
         $hasPort = false;
         $hasPort = false;
-        if (isset($_SERVER['HTTP_HOST'])) {
-            [$host, $port] = self::extractHostAndPortFromAuthority($_SERVER['HTTP_HOST']);
+        $authority = self::getServerParam('HTTP_HOST');
+        if ($authority !== null) {
+            [$host, $port] = self::extractHostAndPortFromAuthority($authority);
             if ($host !== null) {
             if ($host !== null) {
                 $uri = $uri->withHost($host);
                 $uri = $uri->withHost($host);
             }
             }
@@ -214,19 +262,21 @@ class ServerRequest extends Request implements ServerRequestInterface
                 $hasPort = true;
                 $hasPort = true;
                 $uri = $uri->withPort($port);
                 $uri = $uri->withPort($port);
             }
             }
-        } elseif (isset($_SERVER['SERVER_NAME'])) {
-            $uri = $uri->withHost($_SERVER['SERVER_NAME']);
-        } elseif (isset($_SERVER['SERVER_ADDR'])) {
-            $uri = $uri->withHost($_SERVER['SERVER_ADDR']);
+        } elseif (($serverName = self::getServerParam('SERVER_NAME')) !== null) {
+            $uri = $uri->withHost($serverName);
+        } elseif (($serverAddr = self::getServerParam('SERVER_ADDR')) !== null) {
+            $uri = $uri->withHost($serverAddr);
         }
         }
 
 
-        if (!$hasPort && isset($_SERVER['SERVER_PORT'])) {
-            $uri = $uri->withPort($_SERVER['SERVER_PORT']);
+        $serverPort = self::getServerParam('SERVER_PORT');
+        if (!$hasPort && $serverPort !== null && preg_match('/^[+-]?\d+$/D', $serverPort) === 1) {
+            $uri = $uri->withPort((int) $serverPort);
         }
         }
 
 
         $hasQuery = false;
         $hasQuery = false;
-        if (isset($_SERVER['REQUEST_URI'])) {
-            $requestUriParts = explode('?', $_SERVER['REQUEST_URI'], 2);
+        $requestUri = self::getServerParam('REQUEST_URI');
+        if ($requestUri !== null) {
+            $requestUriParts = explode('?', $requestUri, 2);
             $uri = $uri->withPath($requestUriParts[0]);
             $uri = $uri->withPath($requestUriParts[0]);
             if (isset($requestUriParts[1])) {
             if (isset($requestUriParts[1])) {
                 $hasQuery = true;
                 $hasQuery = true;
@@ -234,8 +284,9 @@ class ServerRequest extends Request implements ServerRequestInterface
             }
             }
         }
         }
 
 
-        if (!$hasQuery && isset($_SERVER['QUERY_STRING'])) {
-            $uri = $uri->withQuery($_SERVER['QUERY_STRING']);
+        $queryString = self::getServerParam('QUERY_STRING');
+        if (!$hasQuery && $queryString !== null) {
+            $uri = $uri->withQuery($queryString);
         }
         }
 
 
         return $uri;
         return $uri;
@@ -253,6 +304,37 @@ class ServerRequest extends Request implements ServerRequestInterface
 
 
     public function withUploadedFiles(array $uploadedFiles): ServerRequestInterface
     public function withUploadedFiles(array $uploadedFiles): ServerRequestInterface
     {
     {
+        $invalidUploadedFileFound = false;
+        $invalidUploadedFile = null;
+        $stack = [$uploadedFiles];
+
+        while ($stack !== []) {
+            foreach (\array_pop($stack) as $uploadedFile) {
+                if ($uploadedFile instanceof UploadedFileInterface) {
+                    continue;
+                }
+
+                if (\is_array($uploadedFile)) {
+                    $stack[] = $uploadedFile;
+                    continue;
+                }
+
+                $invalidUploadedFileFound = true;
+                $invalidUploadedFile = $uploadedFile;
+
+                break 2;
+            }
+        }
+
+        if ($invalidUploadedFileFound) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing %s inside ServerRequestInterface::withUploadedFiles() is deprecated; guzzlehttp/psr7 3.0 requires an UploadedFileInterface[] tree.',
+                \get_debug_type($invalidUploadedFile)
+            );
+        }
+
         $new = clone $this;
         $new = clone $this;
         $new->uploadedFiles = $uploadedFiles;
         $new->uploadedFiles = $uploadedFiles;
 
 
@@ -295,6 +377,15 @@ class ServerRequest extends Request implements ServerRequestInterface
 
 
     public function withParsedBody($data): ServerRequestInterface
     public function withParsedBody($data): ServerRequestInterface
     {
     {
+        if ($data !== null && !\is_array($data) && !\is_object($data)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing %s to ServerRequestInterface::withParsedBody() is deprecated; guzzlehttp/psr7 3.0 requires array|object|null.',
+                \get_debug_type($data)
+            );
+        }
+
         $new = clone $this;
         $new = clone $this;
         $new->parsedBody = $data;
         $new->parsedBody = $data;
 
 
@@ -311,6 +402,15 @@ class ServerRequest extends Request implements ServerRequestInterface
      */
      */
     public function getAttribute($attribute, $default = null)
     public function getAttribute($attribute, $default = null)
     {
     {
+        if (!\is_string($attribute)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing %s to ServerRequestInterface::getAttribute() is deprecated; guzzlehttp/psr7 3.0 requires string for $attribute.',
+                \get_debug_type($attribute)
+            );
+        }
+
         if (false === array_key_exists($attribute, $this->attributes)) {
         if (false === array_key_exists($attribute, $this->attributes)) {
             return $default;
             return $default;
         }
         }
@@ -320,6 +420,15 @@ class ServerRequest extends Request implements ServerRequestInterface
 
 
     public function withAttribute($attribute, $value): ServerRequestInterface
     public function withAttribute($attribute, $value): ServerRequestInterface
     {
     {
+        if (!\is_string($attribute)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing %s to ServerRequestInterface::withAttribute() is deprecated; guzzlehttp/psr7 3.0 requires string for $attribute.',
+                \get_debug_type($attribute)
+            );
+        }
+
         $new = clone $this;
         $new = clone $this;
         $new->attributes[$attribute] = $value;
         $new->attributes[$attribute] = $value;
 
 
@@ -328,6 +437,15 @@ class ServerRequest extends Request implements ServerRequestInterface
 
 
     public function withoutAttribute($attribute): ServerRequestInterface
     public function withoutAttribute($attribute): ServerRequestInterface
     {
     {
+        if (!\is_string($attribute)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing %s to ServerRequestInterface::withoutAttribute() is deprecated; guzzlehttp/psr7 3.0 requires string for $attribute.',
+                \get_debug_type($attribute)
+            );
+        }
+
         if (false === array_key_exists($attribute, $this->attributes)) {
         if (false === array_key_exists($attribute, $this->attributes)) {
             return $this;
             return $this;
         }
         }

+ 46 - 1
modules/boonex/xero/plugins/guzzlehttp/psr7/src/Stream.php

@@ -63,7 +63,7 @@ class Stream implements StreamInterface
         $this->seekable = $meta['seekable'];
         $this->seekable = $meta['seekable'];
         $this->readable = (bool) preg_match(self::READABLE_MODES, $meta['mode']);
         $this->readable = (bool) preg_match(self::READABLE_MODES, $meta['mode']);
         $this->writable = (bool) preg_match(self::WRITABLE_MODES, $meta['mode']);
         $this->writable = (bool) preg_match(self::WRITABLE_MODES, $meta['mode']);
-        $this->uri = $this->getMetadata('uri');
+        $this->uri = $meta['uri'] ?? null;
     }
     }
 
 
     /**
     /**
@@ -200,6 +200,24 @@ class Stream implements StreamInterface
 
 
     public function seek($offset, $whence = SEEK_SET): void
     public function seek($offset, $whence = SEEK_SET): void
     {
     {
+        if (!\is_int($offset)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $offset.',
+                \get_debug_type($offset)
+            );
+        }
+
+        if (!\is_int($whence)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $whence.',
+                \get_debug_type($whence)
+            );
+        }
+
         $whence = (int) $whence;
         $whence = (int) $whence;
 
 
         if (!isset($this->stream)) {
         if (!isset($this->stream)) {
@@ -216,6 +234,15 @@ class Stream implements StreamInterface
 
 
     public function read($length): string
     public function read($length): string
     {
     {
+        if (!\is_int($length)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing %s to StreamInterface::read() is deprecated; guzzlehttp/psr7 3.0 requires int for $length.',
+                \get_debug_type($length)
+            );
+        }
+
         if (!isset($this->stream)) {
         if (!isset($this->stream)) {
             throw new \RuntimeException('Stream is detached');
             throw new \RuntimeException('Stream is detached');
         }
         }
@@ -245,6 +272,15 @@ class Stream implements StreamInterface
 
 
     public function write($string): int
     public function write($string): int
     {
     {
+        if (!\is_string($string)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing %s to StreamInterface::write() is deprecated; guzzlehttp/psr7 3.0 requires string for $string.',
+                \get_debug_type($string)
+            );
+        }
+
         if (!isset($this->stream)) {
         if (!isset($this->stream)) {
             throw new \RuntimeException('Stream is detached');
             throw new \RuntimeException('Stream is detached');
         }
         }
@@ -268,6 +304,15 @@ class Stream implements StreamInterface
      */
      */
     public function getMetadata($key = null)
     public function getMetadata($key = null)
     {
     {
+        if ($key !== null && !\is_string($key)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing %s to StreamInterface::getMetadata() is deprecated; guzzlehttp/psr7 3.0 requires string|null for $key.',
+                \get_debug_type($key)
+            );
+        }
+
         if (!isset($this->stream)) {
         if (!isset($this->stream)) {
             return $key ? null : [];
             return $key ? null : [];
         } elseif (!$key) {
         } elseif (!$key) {

+ 45 - 0
modules/boonex/xero/plugins/guzzlehttp/psr7/src/StreamDecoratorTrait.php

@@ -86,6 +86,15 @@ trait StreamDecoratorTrait
      */
      */
     public function getMetadata($key = null)
     public function getMetadata($key = null)
     {
     {
+        if ($key !== null && !\is_string($key)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing %s to StreamInterface::getMetadata() is deprecated; guzzlehttp/psr7 3.0 requires string|null for $key.',
+                \get_debug_type($key)
+            );
+        }
+
         return $this->stream->getMetadata($key);
         return $this->stream->getMetadata($key);
     }
     }
 
 
@@ -131,16 +140,52 @@ trait StreamDecoratorTrait
 
 
     public function seek($offset, $whence = SEEK_SET): void
     public function seek($offset, $whence = SEEK_SET): void
     {
     {
+        if (!\is_int($offset)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $offset.',
+                \get_debug_type($offset)
+            );
+        }
+
+        if (!\is_int($whence)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing %s to StreamInterface::seek() is deprecated; guzzlehttp/psr7 3.0 requires int for $whence.',
+                \get_debug_type($whence)
+            );
+        }
+
         $this->stream->seek($offset, $whence);
         $this->stream->seek($offset, $whence);
     }
     }
 
 
     public function read($length): string
     public function read($length): string
     {
     {
+        if (!\is_int($length)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing %s to StreamInterface::read() is deprecated; guzzlehttp/psr7 3.0 requires int for $length.',
+                \get_debug_type($length)
+            );
+        }
+
         return $this->stream->read($length);
         return $this->stream->read($length);
     }
     }
 
 
     public function write($string): int
     public function write($string): int
     {
     {
+        if (!\is_string($string)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing %s to StreamInterface::write() is deprecated; guzzlehttp/psr7 3.0 requires string for $string.',
+                \get_debug_type($string)
+            );
+        }
+
         return $this->stream->write($string);
         return $this->stream->write($string);
     }
     }
 
 

+ 7 - 1
modules/boonex/xero/plugins/guzzlehttp/psr7/src/StreamWrapper.php

@@ -44,7 +44,13 @@ final class StreamWrapper
                 .'writable, or both.');
                 .'writable, or both.');
         }
         }
 
 
-        return fopen('guzzle://stream', $mode, false, self::createStreamContext($stream));
+        $resource = @fopen('guzzle://stream', $mode, false, self::createStreamContext($stream));
+
+        if ($resource === false) {
+            throw new \RuntimeException('Unable to create stream resource');
+        }
+
+        return $resource;
     }
     }
 
 
     /**
     /**

+ 198 - 57
modules/boonex/xero/plugins/guzzlehttp/psr7/src/Uri.php

@@ -38,20 +38,7 @@ class Uri implements UriInterface, \JsonSerializable
         'ldap' => 389,
         'ldap' => 389,
     ];
     ];
 
 
-    /**
-     * Unreserved characters for use in a regex.
-     *
-     * @see https://datatracker.ietf.org/doc/html/rfc3986#section-2.3
-     */
-    private const CHAR_UNRESERVED = 'a-zA-Z0-9_\-\.~';
-
-    /**
-     * Sub-delims for use in a regex.
-     *
-     * @see https://datatracker.ietf.org/doc/html/rfc3986#section-2.2
-     */
-    private const CHAR_SUB_DELIMS = '!\$&\'\(\)\*\+,;=';
-    private const QUERY_SEPARATORS_REPLACEMENT = ['=' => '%3D', '&' => '%26'];
+    private const QUERY_SEPARATORS_REPLACEMENT = ['=' => '%3D', '&' => '%26', '+' => '%2B'];
 
 
     /** @var string Uri scheme. */
     /** @var string Uri scheme. */
     private $scheme = '';
     private $scheme = '';
@@ -74,9 +61,6 @@ class Uri implements UriInterface, \JsonSerializable
     /** @var string Uri fragment. */
     /** @var string Uri fragment. */
     private $fragment = '';
     private $fragment = '';
 
 
-    /** @var string|null String representation */
-    private $composedComponents;
-
     public function __construct(string $uri = '')
     public function __construct(string $uri = '')
     {
     {
         if ($uri !== '') {
         if ($uri !== '') {
@@ -84,7 +68,13 @@ class Uri implements UriInterface, \JsonSerializable
             if ($parts === false) {
             if ($parts === false) {
                 throw new MalformedUriException("Unable to parse URI: $uri");
                 throw new MalformedUriException("Unable to parse URI: $uri");
             }
             }
-            $this->applyParts($parts);
+            try {
+                $this->applyParts($parts);
+            } catch (MalformedUriException $e) {
+                throw $e;
+            } catch (\InvalidArgumentException $e) {
+                throw new MalformedUriException($e->getMessage(), 0, $e);
+            }
         }
         }
     }
     }
 
 
@@ -105,15 +95,37 @@ class Uri implements UriInterface, \JsonSerializable
      */
      */
     private static function parse(string $url)
     private static function parse(string $url)
     {
     {
-        // If IPv6
+        if (self::isPathNoSchemeReference($url)) {
+            return self::parsePathNoSchemeReference($url);
+        }
+
+        // Preserve bracketed IPv6 literals before encoding, including dotted IPv4
+        // tails. DEL (\x7F) is excluded so a raw-DEL host falls through to the
+        // general path and is rejected rather than silently mutated by parse_url().
         $prefix = '';
         $prefix = '';
-        if (preg_match('%^(.*://\[[0-9:a-fA-F]+\])(.*?)$%', $url, $matches)) {
+        $ipv6Prefix = preg_match('%\A([0-9A-Za-z+.-]+://\[[^\]\x00-\x20\x7F/?#@]+\])(.*)\z%s', $url, $matches);
+
+        if ($ipv6Prefix === false) {
+            return false;
+        }
+
+        if ($ipv6Prefix === 1) {
             /** @var array{0:string, 1:string, 2:string} $matches */
             /** @var array{0:string, 1:string, 2:string} $matches */
+            $suffix = $matches[2];
+
+            // After the bracketed host only an optional numeric port and/or a
+            // path, query, or fragment may follow. Anything else (for example
+            // `:80@evil` or `:80x`) would let parse_url() reinterpret a
+            // different host.
+            if (preg_match('%\A(?::[0-9]*)?(?:[/?#].*)?\z%s', $suffix) !== 1) {
+                return false;
+            }
+
             $prefix = $matches[1];
             $prefix = $matches[1];
-            $url = $matches[2];
+            $url = $suffix;
         }
         }
 
 
-        /** @var string */
+        /** @var string|null */
         $encodedUrl = preg_replace_callback(
         $encodedUrl = preg_replace_callback(
             '%[^:/@?&=#]+%usD',
             '%[^:/@?&=#]+%usD',
             static function ($matches) {
             static function ($matches) {
@@ -122,6 +134,10 @@ class Uri implements UriInterface, \JsonSerializable
             $url
             $url
         );
         );
 
 
+        if ($encodedUrl === null) {
+            return false;
+        }
+
         $result = parse_url($prefix.$encodedUrl);
         $result = parse_url($prefix.$encodedUrl);
 
 
         if ($result === false) {
         if ($result === false) {
@@ -131,19 +147,48 @@ class Uri implements UriInterface, \JsonSerializable
         return array_map('urldecode', $result);
         return array_map('urldecode', $result);
     }
     }
 
 
-    public function __toString(): string
+    private static function isPathNoSchemeReference(string $url): bool
     {
     {
-        if ($this->composedComponents === null) {
-            $this->composedComponents = self::composeComponents(
-                $this->scheme,
-                $this->getAuthority(),
-                $this->path,
-                $this->query,
-                $this->fragment
-            );
+        if ($url === '' || $url[0] === '/' || $url[0] === '?' || $url[0] === '#') {
+            return false;
         }
         }
 
 
-        return $this->composedComponents;
+        $firstSegment = substr($url, 0, strcspn($url, '/?#'));
+
+        return strpos($firstSegment, ':') === false;
+    }
+
+    /**
+     * @return array{path: string, query?: string, fragment?: string}
+     */
+    private static function parsePathNoSchemeReference(string $url): array
+    {
+        $parts = [];
+
+        if (false !== ($fragmentPosition = strpos($url, '#'))) {
+            $parts['fragment'] = substr($url, $fragmentPosition + 1);
+            $url = substr($url, 0, $fragmentPosition);
+        }
+
+        if (false !== ($queryPosition = strpos($url, '?'))) {
+            $parts['query'] = substr($url, $queryPosition + 1);
+            $url = substr($url, 0, $queryPosition);
+        }
+
+        $parts['path'] = $url;
+
+        return $parts;
+    }
+
+    public function __toString(): string
+    {
+        return self::composeComponents(
+            $this->scheme,
+            $this->getAuthority(),
+            $this->path,
+            $this->query,
+            $this->fragment
+        );
     }
     }
 
 
     /**
     /**
@@ -344,12 +389,38 @@ class Uri implements UriInterface, \JsonSerializable
         $result = self::getFilteredQueryString($uri, array_keys($keyValueArray));
         $result = self::getFilteredQueryString($uri, array_keys($keyValueArray));
 
 
         foreach ($keyValueArray as $key => $value) {
         foreach ($keyValueArray as $key => $value) {
-            $result[] = self::generateQueryString((string) $key, $value !== null ? (string) $value : null);
+            $result[] = self::generateQueryString((string) $key, $value !== null ? self::stringifyQueryValue($value) : null);
         }
         }
 
 
         return $uri->withQuery(implode('&', $result));
         return $uri->withQuery(implode('&', $result));
     }
     }
 
 
+    /**
+     * Stringifies a non-null query value, deprecating non-string values that
+     * guzzlehttp/psr7 3.0 will reject. Non-finite floats are normalized to the
+     * strings PHP coerces them to, as implicit coercion of NAN emits a warning
+     * on PHP 8.5.
+     *
+     * @param mixed $value
+     */
+    private static function stringifyQueryValue($value): string
+    {
+        if (!is_string($value)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.12',
+                'Passing %s to Uri::withQueryValues() is deprecated; cast it to a string. guzzlehttp/psr7 3.0 will only accept string or null query values.',
+                \gettype($value)
+            );
+
+            if (is_float($value) && !is_finite($value)) {
+                return is_nan($value) ? 'NAN' : ($value > 0 ? 'INF' : '-INF');
+            }
+        }
+
+        return (string) $value;
+    }
+
     /**
     /**
      * Creates a URI from a hash of `parse_url` components.
      * Creates a URI from a hash of `parse_url` components.
      *
      *
@@ -360,12 +431,54 @@ class Uri implements UriInterface, \JsonSerializable
     public static function fromParts(array $parts): UriInterface
     public static function fromParts(array $parts): UriInterface
     {
     {
         $uri = new self();
         $uri = new self();
-        $uri->applyParts($parts);
-        $uri->validateState();
+        try {
+            $uri->applyParts($parts);
+            $uri->validateState();
+        } catch (MalformedUriException $e) {
+            throw $e;
+        } catch (\InvalidArgumentException $e) {
+            throw new MalformedUriException($e->getMessage(), 0, $e);
+        }
 
 
         return $uri;
         return $uri;
     }
     }
 
 
+    /**
+     * @throws \InvalidArgumentException If the host is invalid.
+     *
+     * @internal
+     */
+    public static function assertValidHost(string $host): void
+    {
+        if ($host === '') {
+            return;
+        }
+
+        // Reject control characters and URI authority delimiters so getHost()
+        // cannot disagree with the on-wire authority.
+        $invalidHost = preg_match('/[\x00-\x20\x7F\/\?#@\\\\]/', $host);
+
+        if ($invalidHost === false) {
+            throw new \RuntimeException('Unable to validate URI host: '.preg_last_error_msg());
+        }
+
+        if ($invalidHost === 1) {
+            throw new \InvalidArgumentException(sprintf('Invalid host: "%s"', $host));
+        }
+
+        if (strpos($host, '[') !== false || strpos($host, ']') !== false) {
+            if ($host[0] !== '[' || substr($host, -1) !== ']') {
+                throw new \InvalidArgumentException(sprintf('Invalid host: "%s"', $host));
+            }
+
+            return;
+        }
+
+        if (strpos($host, ':') !== false) {
+            throw new \InvalidArgumentException(sprintf('Invalid host: "%s"', $host));
+        }
+    }
+
     public function getScheme(): string
     public function getScheme(): string
     {
     {
         return $this->scheme;
         return $this->scheme;
@@ -425,7 +538,6 @@ class Uri implements UriInterface, \JsonSerializable
 
 
         $new = clone $this;
         $new = clone $this;
         $new->scheme = $scheme;
         $new->scheme = $scheme;
-        $new->composedComponents = null;
         $new->removeDefaultPort();
         $new->removeDefaultPort();
         $new->validateState();
         $new->validateState();
 
 
@@ -445,7 +557,6 @@ class Uri implements UriInterface, \JsonSerializable
 
 
         $new = clone $this;
         $new = clone $this;
         $new->userInfo = $info;
         $new->userInfo = $info;
-        $new->composedComponents = null;
         $new->validateState();
         $new->validateState();
 
 
         return $new;
         return $new;
@@ -461,7 +572,6 @@ class Uri implements UriInterface, \JsonSerializable
 
 
         $new = clone $this;
         $new = clone $this;
         $new->host = $host;
         $new->host = $host;
-        $new->composedComponents = null;
         $new->validateState();
         $new->validateState();
 
 
         return $new;
         return $new;
@@ -469,6 +579,15 @@ class Uri implements UriInterface, \JsonSerializable
 
 
     public function withPort($port): UriInterface
     public function withPort($port): UriInterface
     {
     {
+        if ($port !== null && !\is_int($port)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing %s to UriInterface::withPort() is deprecated; guzzlehttp/psr7 3.0 requires int|null.',
+                \get_debug_type($port)
+            );
+        }
+
         $port = $this->filterPort($port);
         $port = $this->filterPort($port);
 
 
         if ($this->port === $port) {
         if ($this->port === $port) {
@@ -477,7 +596,6 @@ class Uri implements UriInterface, \JsonSerializable
 
 
         $new = clone $this;
         $new = clone $this;
         $new->port = $port;
         $new->port = $port;
-        $new->composedComponents = null;
         $new->removeDefaultPort();
         $new->removeDefaultPort();
         $new->validateState();
         $new->validateState();
 
 
@@ -494,7 +612,6 @@ class Uri implements UriInterface, \JsonSerializable
 
 
         $new = clone $this;
         $new = clone $this;
         $new->path = $path;
         $new->path = $path;
-        $new->composedComponents = null;
         $new->validateState();
         $new->validateState();
 
 
         return $new;
         return $new;
@@ -510,7 +627,6 @@ class Uri implements UriInterface, \JsonSerializable
 
 
         $new = clone $this;
         $new = clone $this;
         $new->query = $query;
         $new->query = $query;
-        $new->composedComponents = null;
 
 
         return $new;
         return $new;
     }
     }
@@ -525,7 +641,6 @@ class Uri implements UriInterface, \JsonSerializable
 
 
         $new = clone $this;
         $new = clone $this;
         $new->fragment = $fragment;
         $new->fragment = $fragment;
-        $new->composedComponents = null;
 
 
         return $new;
         return $new;
     }
     }
@@ -581,7 +696,18 @@ class Uri implements UriInterface, \JsonSerializable
             throw new \InvalidArgumentException('Scheme must be a string');
             throw new \InvalidArgumentException('Scheme must be a string');
         }
         }
 
 
-        return \strtr($scheme, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
+        $scheme = Utils::asciiToLower($scheme);
+
+        if ($scheme !== '' && !preg_match('/^[a-z][a-z0-9.+-]*$/D', $scheme)) {
+            \trigger_deprecation(
+                'guzzlehttp/psr7',
+                '2.11',
+                'Passing "%s" as a URI scheme is deprecated; guzzlehttp/psr7 3.0 requires URI schemes to match RFC 3986 syntax and begin with a letter.',
+                $scheme
+            );
+        }
+
+        return $scheme;
     }
     }
 
 
     /**
     /**
@@ -595,10 +721,10 @@ class Uri implements UriInterface, \JsonSerializable
             throw new \InvalidArgumentException('User info must be a string');
             throw new \InvalidArgumentException('User info must be a string');
         }
         }
 
 
-        return preg_replace_callback(
-            '/(?:[^%'.self::CHAR_UNRESERVED.self::CHAR_SUB_DELIMS.']+|%(?![A-Fa-f0-9]{2}))/',
-            [$this, 'rawurlencodeMatchZero'],
-            $component
+        return $this->filterComponent(
+            '/(?:[^%'.Rfc3986::CHAR_UNRESERVED.Rfc3986::CHAR_SUB_DELIMS.']+|%(?![A-Fa-f0-9]{2}))/',
+            $component,
+            'Unable to filter URI user info'
         );
         );
     }
     }
 
 
@@ -613,7 +739,10 @@ class Uri implements UriInterface, \JsonSerializable
             throw new \InvalidArgumentException('Host must be a string');
             throw new \InvalidArgumentException('Host must be a string');
         }
         }
 
 
-        return \strtr($host, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
+        $host = Utils::asciiToLower($host);
+        self::assertValidHost($host);
+
+        return $host;
     }
     }
 
 
     /**
     /**
@@ -661,7 +790,8 @@ class Uri implements UriInterface, \JsonSerializable
 
 
     private static function generateQueryString(string $key, ?string $value): string
     private static function generateQueryString(string $key, ?string $value): string
     {
     {
-        // Query string separators ("=", "&") within the key or value need to be encoded
+        // Query string separators ("=", "&") and literal plus signs ("+") within the
+        // key or value need to be encoded
         // (while preventing double-encoding) before setting the query string. All other
         // (while preventing double-encoding) before setting the query string. All other
         // chars that need percent-encoding will be encoded by withQuery().
         // chars that need percent-encoding will be encoded by withQuery().
         $queryString = strtr($key, self::QUERY_SEPARATORS_REPLACEMENT);
         $queryString = strtr($key, self::QUERY_SEPARATORS_REPLACEMENT);
@@ -693,10 +823,10 @@ class Uri implements UriInterface, \JsonSerializable
             throw new \InvalidArgumentException('Path must be a string');
             throw new \InvalidArgumentException('Path must be a string');
         }
         }
 
 
-        return preg_replace_callback(
-            '/(?:[^'.self::CHAR_UNRESERVED.self::CHAR_SUB_DELIMS.'%:@\/]++|%(?![A-Fa-f0-9]{2}))/',
-            [$this, 'rawurlencodeMatchZero'],
-            $path
+        return $this->filterComponent(
+            '/(?:[^'.Rfc3986::CHAR_UNRESERVED.Rfc3986::CHAR_SUB_DELIMS.'%:@\/]++|%(?![A-Fa-f0-9]{2}))/',
+            $path,
+            'Unable to filter URI path'
         );
         );
     }
     }
 
 
@@ -713,13 +843,24 @@ class Uri implements UriInterface, \JsonSerializable
             throw new \InvalidArgumentException('Query and fragment must be a string');
             throw new \InvalidArgumentException('Query and fragment must be a string');
         }
         }
 
 
-        return preg_replace_callback(
-            '/(?:[^'.self::CHAR_UNRESERVED.self::CHAR_SUB_DELIMS.'%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/',
-            [$this, 'rawurlencodeMatchZero'],
-            $str
+        return $this->filterComponent(
+            '/(?:[^'.Rfc3986::CHAR_UNRESERVED.Rfc3986::CHAR_SUB_DELIMS.'%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/',
+            $str,
+            'Unable to filter URI query or fragment'
         );
         );
     }
     }
 
 
+    private function filterComponent(string $pattern, string $component, string $context): string
+    {
+        $filtered = preg_replace_callback($pattern, [$this, 'rawurlencodeMatchZero'], $component);
+
+        if ($filtered === null) {
+            throw new \RuntimeException($context.': '.preg_last_error_msg());
+        }
+
+        return $filtered;
+    }
+
     private function rawurlencodeMatchZero(array $match): string
     private function rawurlencodeMatchZero(array $match): string
     {
     {
         return rawurlencode($match[0]);
         return rawurlencode($match[0]);

+ 11 - 3
modules/boonex/xero/plugins/guzzlehttp/psr7/src/UriComparator.php

@@ -19,7 +19,7 @@ final class UriComparator
      */
      */
     public static function isCrossOrigin(UriInterface $original, UriInterface $modified): bool
     public static function isCrossOrigin(UriInterface $original, UriInterface $modified): bool
     {
     {
-        if (\strcasecmp($original->getHost(), $modified->getHost()) !== 0) {
+        if (!Utils::caselessEquals($original->getHost(), $modified->getHost())) {
             return true;
             return true;
         }
         }
 
 
@@ -34,7 +34,7 @@ final class UriComparator
         return false;
         return false;
     }
     }
 
 
-    private static function computePort(UriInterface $uri): int
+    private static function computePort(UriInterface $uri): ?int
     {
     {
         $port = $uri->getPort();
         $port = $uri->getPort();
 
 
@@ -42,7 +42,15 @@ final class UriComparator
             return $port;
             return $port;
         }
         }
 
 
-        return 'https' === $uri->getScheme() ? 443 : 80;
+        if ('http' === $uri->getScheme()) {
+            return 80;
+        }
+
+        if ('https' === $uri->getScheme()) {
+            return 443;
+        }
+
+        return null;
     }
     }
 
 
     private function __construct()
     private function __construct()

+ 45 - 14
modules/boonex/xero/plugins/guzzlehttp/psr7/src/UriNormalizer.php

@@ -116,6 +116,11 @@ final class UriNormalizer
      * treated equivalent which is not necessarily true according to RFC 3986. But that difference
      * treated equivalent which is not necessarily true according to RFC 3986. But that difference
      * is highly uncommon in reality. So this potential normalization is implied in PSR-7 as well.
      * is highly uncommon in reality. So this potential normalization is implied in PSR-7 as well.
      *
      *
+     * When a normalization rewrites the path of a relative-path reference so that its first
+     * segment contains a colon, which would be mistaken for a scheme name (RFC 3986 Section 4.2),
+     * the path is prefixed with "./" instead of throwing, e.g. "a%41:" becomes "./aA:" as "aA:"
+     * would be an absolute URI with the scheme "aa".
+     *
      * @param UriInterface $uri   The URI to normalize
      * @param UriInterface $uri   The URI to normalize
      * @param int          $flags A bitmask of normalizations to apply, see constants
      * @param int          $flags A bitmask of normalizations to apply, see constants
      *
      *
@@ -150,7 +155,13 @@ final class UriNormalizer
         }
         }
 
 
         if ($flags & self::REMOVE_DUPLICATE_SLASHES) {
         if ($flags & self::REMOVE_DUPLICATE_SLASHES) {
-            $uri = $uri->withPath(preg_replace('#//++#', '/', $uri->getPath()));
+            $path = preg_replace('#//++#', '/', $uri->getPath());
+
+            if ($path === null) {
+                throw new \RuntimeException('Unable to remove duplicate slashes from URI path: '.preg_last_error_msg());
+            }
+
+            $uri = self::withGuardedPath($uri, $path);
         }
         }
 
 
         if ($flags & self::SORT_QUERY_PARAMETERS && $uri->getQuery() !== '') {
         if ($flags & self::SORT_QUERY_PARAMETERS && $uri->getQuery() !== '') {
@@ -186,15 +197,12 @@ final class UriNormalizer
         $regex = '/(?:%[A-Fa-f0-9]{2})++/';
         $regex = '/(?:%[A-Fa-f0-9]{2})++/';
 
 
         $callback = function (array $match): string {
         $callback = function (array $match): string {
-            return strtoupper($match[0]);
+            return Utils::asciiToUpper($match[0]);
         };
         };
 
 
-        return
-            $uri->withPath(
-                preg_replace_callback($regex, $callback, $uri->getPath())
-            )->withQuery(
-                preg_replace_callback($regex, $callback, $uri->getQuery())
-            );
+        return self::withGuardedPath($uri, self::normalizePercentEncodingInComponent($uri->getPath(), $regex, $callback))
+            ->withQuery(self::normalizePercentEncodingInComponent($uri->getQuery(), $regex, $callback))
+            ->withFragment(self::normalizePercentEncodingInComponent($uri->getFragment(), $regex, $callback));
     }
     }
 
 
     private static function decodeUnreservedCharacters(UriInterface $uri): UriInterface
     private static function decodeUnreservedCharacters(UriInterface $uri): UriInterface
@@ -205,12 +213,35 @@ final class UriNormalizer
             return rawurldecode($match[0]);
             return rawurldecode($match[0]);
         };
         };
 
 
-        return
-            $uri->withPath(
-                preg_replace_callback($regex, $callback, $uri->getPath())
-            )->withQuery(
-                preg_replace_callback($regex, $callback, $uri->getQuery())
-            );
+        return self::withGuardedPath($uri, self::normalizePercentEncodingInComponent($uri->getPath(), $regex, $callback))
+            ->withQuery(self::normalizePercentEncodingInComponent($uri->getQuery(), $regex, $callback))
+            ->withFragment(self::normalizePercentEncodingInComponent($uri->getFragment(), $regex, $callback));
+    }
+
+    /**
+     * Writes the given path only when it differs from the current one, guarded so the write cannot throw.
+     */
+    private static function withGuardedPath(UriInterface $uri, string $path): UriInterface
+    {
+        if ($path === $uri->getPath()) {
+            return $uri;
+        }
+
+        return $uri->withPath(UriResolver::guardedPath($uri, $path));
+    }
+
+    /**
+     * @param callable(array): string $callback
+     */
+    private static function normalizePercentEncodingInComponent(string $component, string $regex, callable $callback): string
+    {
+        $normalized = preg_replace_callback($regex, $callback, $component);
+
+        if ($normalized === null) {
+            throw new \RuntimeException('Unable to normalize URI component percent-encoding: '.preg_last_error_msg());
+        }
+
+        return $normalized;
     }
     }
 
 
     private function __construct()
     private function __construct()

+ 52 - 26
modules/boonex/xero/plugins/guzzlehttp/psr7/src/UriResolver.php

@@ -50,10 +50,36 @@ final class UriResolver
         return $newPath;
         return $newPath;
     }
     }
 
 
+    /**
+     * Returns the path, prefixed with "./" when it would otherwise begin a relative-path reference with a segment
+     * containing a colon.
+     *
+     * Such a segment would be mistaken for a scheme name (RFC 3986 Section 4.2), so a URI without a scheme and
+     * authority cannot hold the path, but reference resolution and percent-encoding normalization can produce one.
+     * The "./" prefix the RFC prescribes resolves back to the same path.
+     *
+     * @see https://datatracker.ietf.org/doc/html/rfc3986#section-4.2
+     *
+     * @internal
+     */
+    public static function guardedPath(UriInterface $uri, string $path): string
+    {
+        if ($uri->getScheme() !== '' || $uri->getAuthority() !== '' || strpos(explode('/', $path, 2)[0], ':') === false) {
+            return $path;
+        }
+
+        return './'.$path;
+    }
+
     /**
     /**
      * Converts the relative URI into a new URI that is resolved against the base URI.
      * Converts the relative URI into a new URI that is resolved against the base URI.
      *
      *
+     * When the resolved path is a relative-path reference whose first segment contains a colon,
+     * which would be mistaken for a scheme name (RFC 3986 Section 4.2), it is prefixed with "./",
+     * e.g. "./a:b".
+     *
      * @see https://datatracker.ietf.org/doc/html/rfc3986#section-5.2
      * @see https://datatracker.ietf.org/doc/html/rfc3986#section-5.2
+     * @see https://datatracker.ietf.org/doc/html/rfc3986#section-4.2
      */
      */
     public static function resolve(UriInterface $base, UriInterface $rel): UriInterface
     public static function resolve(UriInterface $base, UriInterface $rel): UriInterface
     {
     {
@@ -67,41 +93,41 @@ final class UriResolver
         }
         }
 
 
         if ($rel->getAuthority() != '') {
         if ($rel->getAuthority() != '') {
-            $targetAuthority = $rel->getAuthority();
-            $targetPath = self::removeDotSegments($rel->getPath());
-            $targetQuery = $rel->getQuery();
+            return $rel
+                ->withScheme($base->getScheme())
+                ->withPath(self::removeDotSegments($rel->getPath()));
+        }
+
+        if ($rel->getPath() === '') {
+            $targetPath = $base->getPath();
+            $targetQuery = $rel->getQuery() != '' ? $rel->getQuery() : $base->getQuery();
         } else {
         } else {
-            $targetAuthority = $base->getAuthority();
-            if ($rel->getPath() === '') {
-                $targetPath = $base->getPath();
-                $targetQuery = $rel->getQuery() != '' ? $rel->getQuery() : $base->getQuery();
+            if ($rel->getPath()[0] === '/') {
+                $targetPath = $rel->getPath();
             } else {
             } else {
-                if ($rel->getPath()[0] === '/') {
-                    $targetPath = $rel->getPath();
+                if ($base->getAuthority() != '' && $base->getPath() === '') {
+                    $targetPath = '/'.$rel->getPath();
                 } else {
                 } else {
-                    if ($targetAuthority != '' && $base->getPath() === '') {
-                        $targetPath = '/'.$rel->getPath();
+                    $lastSlashPos = strrpos($base->getPath(), '/');
+                    if ($lastSlashPos === false) {
+                        $targetPath = $rel->getPath();
                     } else {
                     } else {
-                        $lastSlashPos = strrpos($base->getPath(), '/');
-                        if ($lastSlashPos === false) {
-                            $targetPath = $rel->getPath();
-                        } else {
-                            $targetPath = substr($base->getPath(), 0, $lastSlashPos + 1).$rel->getPath();
-                        }
+                        $targetPath = substr($base->getPath(), 0, $lastSlashPos + 1).$rel->getPath();
                     }
                     }
                 }
                 }
-                $targetPath = self::removeDotSegments($targetPath);
-                $targetQuery = $rel->getQuery();
             }
             }
+            $targetPath = self::removeDotSegments($targetPath);
+            $targetQuery = $rel->getQuery();
+        }
+
+        if ($targetPath !== $base->getPath()) {
+            $targetPath = self::guardedPath($base, $targetPath);
         }
         }
 
 
-        return new Uri(Uri::composeComponents(
-            $base->getScheme(),
-            $targetAuthority,
-            $targetPath,
-            $targetQuery,
-            $rel->getFragment()
-        ));
+        return $base
+            ->withPath($targetPath)
+            ->withQuery($targetQuery)
+            ->withFragment($rel->getFragment());
     }
     }
 
 
     /**
     /**

+ 288 - 41
modules/boonex/xero/plugins/guzzlehttp/psr7/src/Utils.php

@@ -5,12 +5,70 @@ declare(strict_types=1);
 namespace GuzzleHttp\Psr7;
 namespace GuzzleHttp\Psr7;
 
 
 use Psr\Http\Message\RequestInterface;
 use Psr\Http\Message\RequestInterface;
-use Psr\Http\Message\ServerRequestInterface;
 use Psr\Http\Message\StreamInterface;
 use Psr\Http\Message\StreamInterface;
 use Psr\Http\Message\UriInterface;
 use Psr\Http\Message\UriInterface;
 
 
 final class Utils
 final class Utils
 {
 {
+    /**
+     * Converts ASCII uppercase letters in a string to lowercase.
+     *
+     * Unlike strtolower(), which honors LC_CTYPE before PHP 8.2, the
+     * conversion is locale-independent and leaves every non-ASCII byte
+     * unchanged, as HTTP protocol elements require.
+     */
+    public static function asciiToLower(string $string): string
+    {
+        return strtr($string, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
+    }
+
+    /**
+     * Converts ASCII lowercase letters in a string to uppercase.
+     *
+     * Unlike strtoupper(), which honors LC_CTYPE before PHP 8.2, the
+     * conversion is locale-independent and leaves every non-ASCII byte
+     * unchanged, as HTTP protocol elements require.
+     */
+    public static function asciiToUpper(string $string): string
+    {
+        return strtr($string, 'abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ');
+    }
+
+    /**
+     * Converts the first character of a string to uppercase when it is an
+     * ASCII lowercase letter.
+     *
+     * Unlike ucfirst(), which honors LC_CTYPE before PHP 8.2, the conversion
+     * is locale-independent and leaves every non-ASCII byte unchanged, as
+     * HTTP protocol elements require.
+     */
+    public static function asciiUcFirst(string $string): string
+    {
+        if ($string === '') {
+            return '';
+        }
+
+        return self::asciiToUpper($string[0]).substr($string, 1);
+    }
+
+    /**
+     * Checks whether the haystack contains the needle, comparing ASCII
+     * letters case-insensitively and without locale sensitivity.
+     */
+    public static function caselessContains(string $haystack, string $needle): bool
+    {
+        return str_contains(self::asciiToLower($haystack), self::asciiToLower($needle));
+    }
+
+    /**
+     * Checks whether two strings are equal, comparing ASCII letters
+     * case-insensitively and without locale sensitivity.
+     */
+    public static function caselessEquals(string $left, string $right): bool
+    {
+        return self::asciiToLower($left) === self::asciiToLower($right);
+    }
+
     /**
     /**
      * Remove the items given by the keys, case insensitively from the data.
      * Remove the items given by the keys, case insensitively from the data.
      *
      *
@@ -21,11 +79,11 @@ final class Utils
         $result = [];
         $result = [];
 
 
         foreach ($keys as &$key) {
         foreach ($keys as &$key) {
-            $key = strtolower((string) $key);
+            $key = self::asciiToLower((string) $key);
         }
         }
 
 
         foreach ($data as $k => $v) {
         foreach ($data as $k => $v) {
-            if (!in_array(strtolower((string) $k), $keys)) {
+            if (!in_array(self::asciiToLower((string) $k), $keys)) {
                 $result[$k] = $v;
                 $result[$k] = $v;
             }
             }
         }
         }
@@ -37,6 +95,11 @@ final class Utils
      * Copy the contents of a stream into another stream until the given number
      * Copy the contents of a stream into another stream until the given number
      * of bytes have been read.
      * of bytes have been read.
      *
      *
+     * The copy stops if the destination write returns 0, for example a
+     * BufferStream at its high water mark or a full DroppingStream. For a
+     * guaranteed full copy use a normal writable stream such as a file or
+     * php://temp stream.
+     *
      * @param StreamInterface $source Stream to read from
      * @param StreamInterface $source Stream to read from
      * @param StreamInterface $dest   Stream to write to
      * @param StreamInterface $dest   Stream to write to
      * @param int             $maxLen Maximum number of bytes to read. Pass -1
      * @param int             $maxLen Maximum number of bytes to read. Pass -1
@@ -50,7 +113,12 @@ final class Utils
 
 
         if ($maxLen === -1) {
         if ($maxLen === -1) {
             while (!$source->eof()) {
             while (!$source->eof()) {
-                if (!$dest->write($source->read($bufferSize))) {
+                $buf = $source->read($bufferSize);
+                if ($buf === '') {
+                    break;
+                }
+
+                if (!self::writeAll($dest, $buf)) {
                     break;
                     break;
                 }
                 }
             }
             }
@@ -63,9 +131,33 @@ final class Utils
                     break;
                     break;
                 }
                 }
                 $remaining -= $len;
                 $remaining -= $len;
-                $dest->write($buf);
+                if (!self::writeAll($dest, $buf)) {
+                    break;
+                }
+            }
+        }
+    }
+
+    /**
+     * Writes the full buffer to the destination, retrying short writes.
+     *
+     * Returns false when the destination write returns 0 or less.
+     */
+    private static function writeAll(StreamInterface $dest, string $buf): bool
+    {
+        $written = 0;
+        $len = strlen($buf);
+
+        while ($written < $len) {
+            $result = $dest->write(substr($buf, $written));
+            if ($result <= 0) {
+                return false;
             }
             }
+
+            $written += $result;
         }
         }
+
+        return true;
     }
     }
 
 
     /**
     /**
@@ -146,9 +238,14 @@ final class Utils
      *
      *
      * The changes can be one of:
      * The changes can be one of:
      * - method: (string) Changes the HTTP method.
      * - method: (string) Changes the HTTP method.
-     * - set_headers: (array) Sets the given headers.
-     * - remove_headers: (array) Remove the given headers.
-     * - body: (mixed) Sets the given body.
+     * - set_headers: (array) Sets the given headers. Values must be strings
+     *   or non-empty arrays of strings.
+     * - remove_headers: (array) Remove the given headers. Values may be
+     *   strings or integers.
+     * - body: (mixed) Sets the given body. Present non-null values are converted
+     *   with self::streamFor(), including scalar values, resources, streams,
+     *   iterators, callable arrays, closures, invokable objects, and objects
+     *   with __toString(). String inputs remain literal bodies.
      * - uri: (UriInterface) Set the URI.
      * - uri: (UriInterface) Set the URI.
      * - query: (string) Set the query string value of the URI.
      * - query: (string) Set the query string value of the URI.
      * - version: (string) Set the protocol version.
      * - version: (string) Set the protocol version.
@@ -162,13 +259,26 @@ final class Utils
             return $request;
             return $request;
         }
         }
 
 
+        self::warnOnInvalidModifyRequestChanges($changes);
+
         $headers = $request->getHeaders();
         $headers = $request->getHeaders();
 
 
         if (!isset($changes['uri'])) {
         if (!isset($changes['uri'])) {
             $uri = $request->getUri();
             $uri = $request->getUri();
         } else {
         } else {
             // Remove the host header if one is on the URI
             // Remove the host header if one is on the URI
-            if ($host = $changes['uri']->getHost()) {
+            $host = $changes['uri']->getHost();
+            if ($host !== '') {
+                if (isset($changes['set_headers']) && is_array($changes['set_headers'])) {
+                    foreach (array_keys($changes['set_headers']) as $header) {
+                        if (self::asciiToLower((string) $header) === 'host') {
+                            throw new \InvalidArgumentException(
+                                'Cannot modify request with both a URI containing a host and an explicit Host header.'
+                            );
+                        }
+                    }
+                }
+
                 $changes['set_headers']['Host'] = $host;
                 $changes['set_headers']['Host'] = $host;
 
 
                 if ($port = $changes['uri']->getPort()) {
                 if ($port = $changes['uri']->getPort()) {
@@ -195,33 +305,150 @@ final class Utils
             $uri = $uri->withQuery($changes['query']);
             $uri = $uri->withQuery($changes['query']);
         }
         }
 
 
-        if ($request instanceof ServerRequestInterface) {
-            $new = (new ServerRequest(
-                $changes['method'] ?? $request->getMethod(),
-                $uri,
-                $headers,
-                $changes['body'] ?? $request->getBody(),
-                $changes['version'] ?? $request->getProtocolVersion(),
-                $request->getServerParams()
-            ))
-            ->withParsedBody($request->getParsedBody())
-            ->withQueryParams($request->getQueryParams())
-            ->withCookieParams($request->getCookieParams())
-            ->withUploadedFiles($request->getUploadedFiles());
-
-            foreach ($request->getAttributes() as $key => $value) {
-                $new = $new->withAttribute($key, $value);
+        $hasHost = false;
+        foreach (array_keys($headers) as $header) {
+            if (self::asciiToLower((string) $header) === 'host') {
+                $hasHost = true;
+                break;
+            }
+        }
+
+        // Match Request::__construct() by adding a Host header when one is not provided.
+        if (!$hasHost && $uri->getHost() !== '') {
+            $host = $uri->getHost();
+
+            if (($port = $uri->getPort()) !== null) {
+                $host .= ':'.$port;
             }
             }
 
 
-            return $new;
+            $headers = ['Host' => [$host]] + $headers;
+        }
+
+        $new = $request;
+
+        if (isset($changes['method'])) {
+            $new = $new->withMethod($changes['method']);
         }
         }
 
 
-        return new Request(
-            $changes['method'] ?? $request->getMethod(),
-            $uri,
-            $headers,
-            $changes['body'] ?? $request->getBody(),
-            $changes['version'] ?? $request->getProtocolVersion()
+        if (isset($changes['uri']) || isset($changes['query'])) {
+            $new = $new->withUri($uri, true);
+        }
+
+        if ($headers !== $new->getHeaders()) {
+            foreach (array_keys($new->getHeaders()) as $header) {
+                /** @var RequestInterface */
+                $new = $new->withoutHeader((string) $header);
+            }
+
+            $addedHeaders = [];
+            foreach ($headers as $header => $value) {
+                $header = (string) $header;
+                $normalized = self::asciiToLower($header);
+
+                if (isset($addedHeaders[$normalized])) {
+                    /** @var RequestInterface */
+                    $new = $new->withAddedHeader($addedHeaders[$normalized], $value);
+                } else {
+                    /** @var RequestInterface */
+                    $new = $new->withHeader($header, $value);
+                    $addedHeaders[$normalized] = $header;
+                }
+            }
+        }
+
+        if (isset($changes['body'])) {
+            /** @var RequestInterface */
+            $new = $new->withBody(self::streamFor($changes['body']));
+        }
+
+        if (isset($changes['version'])) {
+            /** @var RequestInterface */
+            $new = $new->withProtocolVersion($changes['version']);
+        }
+
+        return $new;
+    }
+
+    /**
+     * @param array<array-key, mixed> $changes
+     */
+    private static function warnOnInvalidModifyRequestChanges(array $changes): void
+    {
+        foreach (['method', 'query', 'version'] as $key) {
+            if (\array_key_exists($key, $changes) && !\is_string($changes[$key])) {
+                self::warnOnInvalidModifyRequestChange($key, 'string', $changes[$key]);
+            }
+        }
+
+        if (\array_key_exists('uri', $changes) && !$changes['uri'] instanceof UriInterface) {
+            self::warnOnInvalidModifyRequestChange('uri', 'UriInterface', $changes['uri']);
+        }
+
+        if (\array_key_exists('body', $changes) && $changes['body'] === null) {
+            self::warnOnInvalidModifyRequestChange('body', 'resource|string|int|float|bool|StreamInterface|callable|\Iterator|\Stringable', $changes['body']);
+        }
+
+        if (\array_key_exists('set_headers', $changes)) {
+            if (!\is_array($changes['set_headers'])) {
+                self::warnOnInvalidModifyRequestChange('set_headers', 'array<array-key, string|non-empty-array<array-key, string>>', $changes['set_headers']);
+            } else {
+                foreach ($changes['set_headers'] as $header => $value) {
+                    $headerPath = \sprintf('set_headers.%s', (string) $header);
+
+                    if (\is_array($value)) {
+                        if ($value === []) {
+                            self::warnOnInvalidModifyRequestChange($headerPath, 'string|non-empty-array<array-key, string>', $value);
+
+                            break;
+                        }
+
+                        foreach ($value as $index => $item) {
+                            if (!\is_string($item)) {
+                                self::warnOnInvalidModifyRequestChange(\sprintf('%s.%s', $headerPath, (string) $index), 'string', $item);
+
+                                break 2;
+                            }
+                        }
+                    } elseif (!\is_string($value)) {
+                        self::warnOnInvalidModifyRequestChange($headerPath, 'string|non-empty-array<array-key, string>', $value);
+
+                        break;
+                    }
+                }
+            }
+        }
+
+        if (!\array_key_exists('remove_headers', $changes)) {
+            return;
+        }
+
+        if (!\is_array($changes['remove_headers'])) {
+            self::warnOnInvalidModifyRequestChange('remove_headers', 'array<array-key, string|int>', $changes['remove_headers']);
+
+            return;
+        }
+
+        foreach ($changes['remove_headers'] as $index => $header) {
+            if (!\is_string($header) && !\is_int($header)) {
+                self::warnOnInvalidModifyRequestChange(\sprintf('remove_headers.%s', (string) $index), 'string|int', $header);
+
+                return;
+            }
+        }
+    }
+
+    /**
+     * @param mixed $value
+     */
+    private static function warnOnInvalidModifyRequestChange(string $key, string $expected, $value): void
+    {
+        \trigger_deprecation(
+            'guzzlehttp/psr7',
+            '2.11',
+            'Passing %s to Utils::modifyRequest() change "%s" is deprecated; guzzlehttp/psr7 3.0 requires %s.',
+            \get_debug_type($value),
+            $key,
+            $expected
         );
         );
     }
     }
 
 
@@ -285,13 +512,17 @@ final class Utils
      *   the object will be cast to a string and then a stream will be returned that
      *   the object will be cast to a string and then a stream will be returned that
      *   uses the string value.
      *   uses the string value.
      * - `NULL`: When `null` is passed, an empty stream object is returned.
      * - `NULL`: When `null` is passed, an empty stream object is returned.
-     * - `callable` When a callable is passed, a read-only stream object will be
-     *   created that invokes the given callable. The callable is invoked with the
-     *   number of suggested bytes to read. The callable can return any number of
-     *   bytes, but MUST return `false` when there is no more data to return. The
-     *   stream object that wraps the callable will invoke the callable until the
-     *   number of requested bytes are available. Any additional bytes will be
-     *   buffered and used in subsequent reads.
+     * - `callable`: When a callable array, closure, or invokable object is passed
+     *   and no earlier resource or object rule applies, a read-only stream object
+     *   will be created that invokes the given callable. The callable is invoked
+     *   with the suggested number of bytes to read. The callable can return fewer
+     *   or more bytes than requested, but MUST return `false` or `null` when there
+     *   is no more data to return. Any additional bytes will be buffered and used
+     *   in subsequent reads. String inputs are always treated as string bodies,
+     *   even when they name callable functions.
+     *
+     * Passing a non-string scalar (`int`, `float`, or `bool`) is deprecated; cast
+     * it to a string instead. guzzlehttp/psr7 3.0 will reject non-string scalars.
      *
      *
      * @param resource|string|int|float|bool|StreamInterface|callable|\Iterator|null $resource Entity body data
      * @param resource|string|int|float|bool|StreamInterface|callable|\Iterator|null $resource Entity body data
      * @param array{size?: int, metadata?: array}                                    $options  Additional options
      * @param array{size?: int, metadata?: array}                                    $options  Additional options
@@ -301,6 +532,22 @@ final class Utils
     public static function streamFor($resource = '', array $options = []): StreamInterface
     public static function streamFor($resource = '', array $options = []): StreamInterface
     {
     {
         if (is_scalar($resource)) {
         if (is_scalar($resource)) {
+            if (!is_string($resource)) {
+                \trigger_deprecation(
+                    'guzzlehttp/psr7',
+                    '2.12',
+                    'Passing %s to Utils::streamFor() is deprecated; cast it to a string. guzzlehttp/psr7 3.0 will only accept string, resource, StreamInterface, Stringable, Iterator, callable, or null.',
+                    \gettype($resource)
+                );
+
+                if (is_float($resource) && !is_finite($resource)) {
+                    // Normalized only to avoid PHP 8.5's (string) NAN warning
+                    // while deprecated; 3.0 rejects non-finite floats with every
+                    // other non-string scalar.
+                    $resource = is_nan($resource) ? 'NAN' : ($resource > 0 ? 'INF' : '-INF');
+                }
+            }
+
             $stream = self::tryFopen('php://temp', 'r+');
             $stream = self::tryFopen('php://temp', 'r+');
             if ($resource !== '') {
             if ($resource !== '') {
                 fwrite($stream, (string) $resource);
                 fwrite($stream, (string) $resource);
@@ -397,7 +644,7 @@ final class Utils
         restore_error_handler();
         restore_error_handler();
 
 
         if ($ex) {
         if ($ex) {
-            /** @var $ex \RuntimeException */
+            /** @var \RuntimeException $ex */
             throw $ex;
             throw $ex;
         }
         }
 
 
@@ -444,7 +691,7 @@ final class Utils
         restore_error_handler();
         restore_error_handler();
 
 
         if ($ex) {
         if ($ex) {
-            /** @var $ex \RuntimeException */
+            /** @var \RuntimeException $ex */
             throw $ex;
             throw $ex;
         }
         }
 
 

+ 1 - 0
modules/boonex/xero/plugins/league/oauth2-client/README.md

@@ -24,6 +24,7 @@ This package is compliant with [PSR-1][], [PSR-2][], [PSR-4][], and [PSR-7][]. I
 
 
 We support the following versions of PHP:
 We support the following versions of PHP:
 
 
+* PHP 8.5
 * PHP 8.4
 * PHP 8.4
 * PHP 8.3
 * PHP 8.3
 * PHP 8.2
 * PHP 8.2

+ 3 - 2
modules/boonex/xero/plugins/league/oauth2-client/composer.json

@@ -6,9 +6,10 @@
         "sort-packages": true
         "sort-packages": true
     },
     },
     "require": {
     "require": {
-        "php": "^7.1 || >=8.0.0 <8.5.0",
+        "php": "^7.1 || >=8.0.0 <8.6.0",
         "ext-json": "*",
         "ext-json": "*",
-        "guzzlehttp/guzzle": "^6.5.8 || ^7.4.5"
+        "guzzlehttp/guzzle": "^6.5.8 || ^7.8.2 || ^8.0",
+        "guzzlehttp/psr7": "^1.9.1 || ^2.6.3 || ^3.0"
     },
     },
     "require-dev": {
     "require-dev": {
         "mockery/mockery": "^1.3.5",
         "mockery/mockery": "^1.3.5",

+ 1 - 1
modules/boonex/xero/plugins/symfony/deprecation-contracts/composer.json

@@ -25,7 +25,7 @@
     "minimum-stability": "dev",
     "minimum-stability": "dev",
     "extra": {
     "extra": {
         "branch-alias": {
         "branch-alias": {
-            "dev-main": "3.6-dev"
+            "dev-main": "3.7-dev"
         },
         },
         "thanks": {
         "thanks": {
             "name": "symfony/contracts",
             "name": "symfony/contracts",

+ 19 - 0
modules/boonex/xero/plugins/symfony/polyfill-php80/LICENSE

@@ -0,0 +1,19 @@
+Copyright (c) 2020-present Fabien Potencier
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is furnished
+to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.

+ 115 - 0
modules/boonex/xero/plugins/symfony/polyfill-php80/Php80.php

@@ -0,0 +1,115 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Polyfill\Php80;
+
+/**
+ * @author Ion Bazan <ion.bazan@gmail.com>
+ * @author Nico Oelgart <nicoswd@gmail.com>
+ * @author Nicolas Grekas <p@tchwork.com>
+ *
+ * @internal
+ */
+final class Php80
+{
+    public static function fdiv(float $dividend, float $divisor): float
+    {
+        return @($dividend / $divisor);
+    }
+
+    public static function get_debug_type($value): string
+    {
+        switch (true) {
+            case null === $value: return 'null';
+            case \is_bool($value): return 'bool';
+            case \is_string($value): return 'string';
+            case \is_array($value): return 'array';
+            case \is_int($value): return 'int';
+            case \is_float($value): return 'float';
+            case \is_object($value): break;
+            case $value instanceof \__PHP_Incomplete_Class: return '__PHP_Incomplete_Class';
+            default:
+                if (null === $type = @get_resource_type($value)) {
+                    return 'unknown';
+                }
+
+                if ('Unknown' === $type) {
+                    $type = 'closed';
+                }
+
+                return "resource ($type)";
+        }
+
+        $class = \get_class($value);
+
+        if (false === strpos($class, '@')) {
+            return $class;
+        }
+
+        return (get_parent_class($class) ?: key(class_implements($class)) ?: 'class').'@anonymous';
+    }
+
+    public static function get_resource_id($res): int
+    {
+        if (!\is_resource($res) && null === @get_resource_type($res)) {
+            throw new \TypeError(\sprintf('Argument 1 passed to get_resource_id() must be of the type resource, %s given', get_debug_type($res)));
+        }
+
+        return (int) $res;
+    }
+
+    public static function preg_last_error_msg(): string
+    {
+        switch (preg_last_error()) {
+            case \PREG_INTERNAL_ERROR:
+                return 'Internal error';
+            case \PREG_BAD_UTF8_ERROR:
+                return 'Malformed UTF-8 characters, possibly incorrectly encoded';
+            case \PREG_BAD_UTF8_OFFSET_ERROR:
+                return 'The offset did not correspond to the beginning of a valid UTF-8 code point';
+            case \PREG_BACKTRACK_LIMIT_ERROR:
+                return 'Backtrack limit exhausted';
+            case \PREG_RECURSION_LIMIT_ERROR:
+                return 'Recursion limit exhausted';
+            case \PREG_JIT_STACKLIMIT_ERROR:
+                return 'JIT stack limit exhausted';
+            case \PREG_NO_ERROR:
+                return 'No error';
+            default:
+                return 'Unknown error';
+        }
+    }
+
+    public static function str_contains(string $haystack, string $needle): bool
+    {
+        return '' === $needle || false !== strpos($haystack, $needle);
+    }
+
+    public static function str_starts_with(string $haystack, string $needle): bool
+    {
+        return 0 === strncmp($haystack, $needle, \strlen($needle));
+    }
+
+    public static function str_ends_with(string $haystack, string $needle): bool
+    {
+        if ('' === $needle || $needle === $haystack) {
+            return true;
+        }
+
+        if ('' === $haystack) {
+            return false;
+        }
+
+        $needleLength = \strlen($needle);
+
+        return $needleLength <= \strlen($haystack) && 0 === substr_compare($haystack, $needle, -$needleLength);
+    }
+}

+ 108 - 0
modules/boonex/xero/plugins/symfony/polyfill-php80/PhpToken.php

@@ -0,0 +1,108 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Polyfill\Php80;
+
+/**
+ * @author Fedonyuk Anton <info@ensostudio.ru>
+ *
+ * @internal
+ */
+class PhpToken implements \Stringable
+{
+    /**
+     * @var int
+     */
+    public $id;
+
+    /**
+     * @var string
+     */
+    public $text;
+
+    /**
+     * @var -1|positive-int
+     */
+    public $line;
+
+    /**
+     * @var int
+     */
+    public $pos;
+
+    /**
+     * @param -1|positive-int $line
+     */
+    public function __construct(int $id, string $text, int $line = -1, int $position = -1)
+    {
+        $this->id = $id;
+        $this->text = $text;
+        $this->line = $line;
+        $this->pos = $position;
+    }
+
+    public function getTokenName(): ?string
+    {
+        if ($this->id < 256) {
+            return \chr($this->id & 0xFF);
+        }
+
+        return 'UNKNOWN' === ($name = token_name($this->id)) ? null : $name;
+    }
+
+    /**
+     * @param int|string|array $kind
+     */
+    public function is($kind): bool
+    {
+        foreach ((array) $kind as $value) {
+            if (\in_array($value, [$this->id, $this->text], true)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    public function isIgnorable(): bool
+    {
+        return \in_array($this->id, [\T_WHITESPACE, \T_COMMENT, \T_DOC_COMMENT, \T_OPEN_TAG], true);
+    }
+
+    public function __toString(): string
+    {
+        return (string) $this->text;
+    }
+
+    /**
+     * @return list<static>
+     */
+    public static function tokenize(string $code, int $flags = 0): array
+    {
+        $line = 1;
+        $position = 0;
+        $tokens = token_get_all($code, $flags);
+        foreach ($tokens as $index => $token) {
+            if (\is_string($token)) {
+                // b" is the only such token longer than one char
+                $id = \ord($token[-1]);
+                $text = $token;
+            } else {
+                [$id, $text, $line] = $token;
+            }
+            $tokens[$index] = new static($id, $text, $line, $position);
+            $position += \strlen($text);
+            $line += preg_match_all('/\r\n?|\n/', $text);
+        }
+
+        return $tokens;
+    }
+}

+ 25 - 0
modules/boonex/xero/plugins/symfony/polyfill-php80/README.md

@@ -0,0 +1,25 @@
+Symfony Polyfill / Php80
+========================
+
+This component provides features added to PHP 8.0 core:
+
+- [`Stringable`](https://php.net/stringable) interface
+- [`fdiv`](https://php.net/fdiv)
+- [`ValueError`](https://php.net/valueerror) class
+- [`UnhandledMatchError`](https://php.net/unhandledmatcherror) class
+- `FILTER_VALIDATE_BOOL` constant
+- [`get_debug_type`](https://php.net/get_debug_type)
+- [`PhpToken`](https://php.net/phptoken) class
+- [`preg_last_error_msg`](https://php.net/preg_last_error_msg)
+- [`str_contains`](https://php.net/str_contains)
+- [`str_starts_with`](https://php.net/str_starts_with)
+- [`str_ends_with`](https://php.net/str_ends_with)
+- [`get_resource_id`](https://php.net/get_resource_id)
+
+More information can be found in the
+[main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md).
+
+License
+=======
+
+This library is released under the [MIT license](LICENSE).

+ 31 - 0
modules/boonex/xero/plugins/symfony/polyfill-php80/Resources/stubs/Attribute.php

@@ -0,0 +1,31 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+#[Attribute(Attribute::TARGET_CLASS)]
+final class Attribute
+{
+    public const TARGET_CLASS = 1;
+    public const TARGET_FUNCTION = 2;
+    public const TARGET_METHOD = 4;
+    public const TARGET_PROPERTY = 8;
+    public const TARGET_CLASS_CONSTANT = 16;
+    public const TARGET_PARAMETER = 32;
+    public const TARGET_ALL = 63;
+    public const IS_REPEATABLE = 64;
+
+    /** @var int */
+    public $flags;
+
+    public function __construct(int $flags = self::TARGET_ALL)
+    {
+        $this->flags = $flags;
+    }
+}

+ 16 - 0
modules/boonex/xero/plugins/symfony/polyfill-php80/Resources/stubs/PhpToken.php

@@ -0,0 +1,16 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+if (\PHP_VERSION_ID < 80000 && extension_loaded('tokenizer')) {
+    class PhpToken extends Symfony\Polyfill\Php80\PhpToken
+    {
+    }
+}

+ 20 - 0
modules/boonex/xero/plugins/symfony/polyfill-php80/Resources/stubs/Stringable.php

@@ -0,0 +1,20 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+if (\PHP_VERSION_ID < 80000) {
+    interface Stringable
+    {
+        /**
+         * @return string
+         */
+        public function __toString();
+    }
+}

+ 16 - 0
modules/boonex/xero/plugins/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php

@@ -0,0 +1,16 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+if (\PHP_VERSION_ID < 80000) {
+    class UnhandledMatchError extends Error
+    {
+    }
+}

+ 16 - 0
modules/boonex/xero/plugins/symfony/polyfill-php80/Resources/stubs/ValueError.php

@@ -0,0 +1,16 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+if (\PHP_VERSION_ID < 80000) {
+    class ValueError extends Error
+    {
+    }
+}

+ 42 - 0
modules/boonex/xero/plugins/symfony/polyfill-php80/bootstrap.php

@@ -0,0 +1,42 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+use Symfony\Polyfill\Php80 as p;
+
+if (\PHP_VERSION_ID >= 80000) {
+    return;
+}
+
+if (!defined('FILTER_VALIDATE_BOOL') && defined('FILTER_VALIDATE_BOOLEAN')) {
+    define('FILTER_VALIDATE_BOOL', \FILTER_VALIDATE_BOOLEAN);
+}
+
+if (!function_exists('fdiv')) {
+    function fdiv(float $num1, float $num2): float { return p\Php80::fdiv($num1, $num2); }
+}
+if (!function_exists('preg_last_error_msg')) {
+    function preg_last_error_msg(): string { return p\Php80::preg_last_error_msg(); }
+}
+if (!function_exists('str_contains')) {
+    function str_contains(?string $haystack, ?string $needle): bool { return p\Php80::str_contains($haystack ?? '', $needle ?? ''); }
+}
+if (!function_exists('str_starts_with')) {
+    function str_starts_with(?string $haystack, ?string $needle): bool { return p\Php80::str_starts_with($haystack ?? '', $needle ?? ''); }
+}
+if (!function_exists('str_ends_with')) {
+    function str_ends_with(?string $haystack, ?string $needle): bool { return p\Php80::str_ends_with($haystack ?? '', $needle ?? ''); }
+}
+if (!function_exists('get_debug_type')) {
+    function get_debug_type($value): string { return p\Php80::get_debug_type($value); }
+}
+if (!function_exists('get_resource_id')) {
+    function get_resource_id($resource): int { return p\Php80::get_resource_id($resource); }
+}

+ 37 - 0
modules/boonex/xero/plugins/symfony/polyfill-php80/composer.json

@@ -0,0 +1,37 @@
+{
+    "name": "symfony/polyfill-php80",
+    "type": "library",
+    "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
+    "keywords": ["polyfill", "shim", "compatibility", "portable"],
+    "homepage": "https://symfony.com",
+    "license": "MIT",
+    "authors": [
+        {
+            "name": "Ion Bazan",
+            "email": "ion.bazan@gmail.com"
+        },
+        {
+            "name": "Nicolas Grekas",
+            "email": "p@tchwork.com"
+        },
+        {
+            "name": "Symfony Community",
+            "homepage": "https://symfony.com/contributors"
+        }
+    ],
+    "require": {
+        "php": ">=7.2"
+    },
+    "autoload": {
+        "psr-4": { "Symfony\\Polyfill\\Php80\\": "" },
+        "files": [ "bootstrap.php" ],
+        "classmap": [ "Resources/stubs" ]
+    },
+    "minimum-stability": "dev",
+    "extra": {
+        "thanks": {
+            "name": "symfony/polyfill",
+            "url": "https://github.com/symfony/polyfill"
+        }
+    }
+}

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません