Gogs 3 weeks ago
parent
commit
80debef568
100 changed files with 9815 additions and 0 deletions
  1. 5 0
      composer.json
  2. 370 0
      composer.lock
  3. 25 0
      vendor/autoload.php
  4. 579 0
      vendor/composer/ClassLoader.php
  5. 359 0
      vendor/composer/InstalledVersions.php
  6. 21 0
      vendor/composer/LICENSE
  7. 10 0
      vendor/composer/autoload_classmap.php
  8. 9 0
      vendor/composer/autoload_namespaces.php
  9. 14 0
      vendor/composer/autoload_psr4.php
  10. 38 0
      vendor/composer/autoload_real.php
  11. 60 0
      vendor/composer/autoload_static.php
  12. 375 0
      vendor/composer/installed.json
  13. 77 0
      vendor/composer/installed.php
  14. 26 0
      vendor/composer/platform_check.php
  15. 30 0
      vendor/phrity/net-uri/composer.json
  16. 486 0
      vendor/phrity/net-uri/src/Uri.php
  17. 31 0
      vendor/phrity/net-uri/src/UriFactory.php
  18. 93 0
      vendor/phrity/util-errorhandler/.github/workflows/acceptance.yml
  19. 6 0
      vendor/phrity/util-errorhandler/.gitignore
  20. 41 0
      vendor/phrity/util-errorhandler/Makefile
  21. 147 0
      vendor/phrity/util-errorhandler/README.md
  22. 33 0
      vendor/phrity/util-errorhandler/composer.json
  23. 13 0
      vendor/phrity/util-errorhandler/phpunit.xml.dist
  24. 121 0
      vendor/phrity/util-errorhandler/src/ErrorHandler.php
  25. 303 0
      vendor/phrity/util-errorhandler/tests/ErrorHandlerTest.php
  26. 21 0
      vendor/psr/http-factory/LICENSE
  27. 12 0
      vendor/psr/http-factory/README.md
  28. 35 0
      vendor/psr/http-factory/composer.json
  29. 18 0
      vendor/psr/http-factory/src/RequestFactoryInterface.php
  30. 18 0
      vendor/psr/http-factory/src/ResponseFactoryInterface.php
  31. 24 0
      vendor/psr/http-factory/src/ServerRequestFactoryInterface.php
  32. 45 0
      vendor/psr/http-factory/src/StreamFactoryInterface.php
  33. 34 0
      vendor/psr/http-factory/src/UploadedFileFactoryInterface.php
  34. 17 0
      vendor/psr/http-factory/src/UriFactoryInterface.php
  35. 36 0
      vendor/psr/http-message/CHANGELOG.md
  36. 19 0
      vendor/psr/http-message/LICENSE
  37. 16 0
      vendor/psr/http-message/README.md
  38. 26 0
      vendor/psr/http-message/composer.json
  39. 130 0
      vendor/psr/http-message/docs/PSR7-Interfaces.md
  40. 159 0
      vendor/psr/http-message/docs/PSR7-Usage.md
  41. 189 0
      vendor/psr/http-message/src/MessageInterface.php
  42. 131 0
      vendor/psr/http-message/src/RequestInterface.php
  43. 70 0
      vendor/psr/http-message/src/ResponseInterface.php
  44. 263 0
      vendor/psr/http-message/src/ServerRequestInterface.php
  45. 160 0
      vendor/psr/http-message/src/StreamInterface.php
  46. 125 0
      vendor/psr/http-message/src/UploadedFileInterface.php
  47. 326 0
      vendor/psr/http-message/src/UriInterface.php
  48. 19 0
      vendor/psr/log/LICENSE
  49. 58 0
      vendor/psr/log/README.md
  50. 26 0
      vendor/psr/log/composer.json
  51. 15 0
      vendor/psr/log/src/AbstractLogger.php
  52. 7 0
      vendor/psr/log/src/InvalidArgumentException.php
  53. 18 0
      vendor/psr/log/src/LogLevel.php
  54. 14 0
      vendor/psr/log/src/LoggerAwareInterface.php
  55. 22 0
      vendor/psr/log/src/LoggerAwareTrait.php
  56. 97 0
      vendor/psr/log/src/LoggerInterface.php
  57. 98 0
      vendor/psr/log/src/LoggerTrait.php
  58. 26 0
      vendor/psr/log/src/NullLogger.php
  59. 21 0
      vendor/textalk/websocket/.github/ISSUE_TEMPLATE/bug_report.md
  60. 14 0
      vendor/textalk/websocket/.github/ISSUE_TEMPLATE/feature_request.md
  61. 10 0
      vendor/textalk/websocket/.github/ISSUE_TEMPLATE/other-issue.md
  62. 97 0
      vendor/textalk/websocket/.github/workflows/acceptance.yml
  63. 6 0
      vendor/textalk/websocket/.gitignore
  64. 16 0
      vendor/textalk/websocket/COPYING.md
  65. 32 0
      vendor/textalk/websocket/Makefile
  66. 67 0
      vendor/textalk/websocket/README.md
  67. 36 0
      vendor/textalk/websocket/composer.json
  68. 167 0
      vendor/textalk/websocket/docs/Changelog.md
  69. 137 0
      vendor/textalk/websocket/docs/Client.md
  70. 51 0
      vendor/textalk/websocket/docs/Contributing.md
  71. 101 0
      vendor/textalk/websocket/docs/Examples.md
  72. 60 0
      vendor/textalk/websocket/docs/Message.md
  73. 136 0
      vendor/textalk/websocket/docs/Server.md
  74. 87 0
      vendor/textalk/websocket/examples/echoserver.php
  75. 94 0
      vendor/textalk/websocket/examples/random_client.php
  76. 93 0
      vendor/textalk/websocket/examples/random_server.php
  77. 51 0
      vendor/textalk/websocket/examples/send.php
  78. 14 0
      vendor/textalk/websocket/lib/BadOpcodeException.php
  79. 7 0
      vendor/textalk/websocket/lib/BadUriException.php
  80. 490 0
      vendor/textalk/websocket/lib/Client.php
  81. 518 0
      vendor/textalk/websocket/lib/Connection.php
  82. 33 0
      vendor/textalk/websocket/lib/ConnectionException.php
  83. 7 0
      vendor/textalk/websocket/lib/Exception.php
  84. 15 0
      vendor/textalk/websocket/lib/Message/Binary.php
  85. 15 0
      vendor/textalk/websocket/lib/Message/Close.php
  86. 32 0
      vendor/textalk/websocket/lib/Message/Factory.php
  87. 74 0
      vendor/textalk/websocket/lib/Message/Message.php
  88. 15 0
      vendor/textalk/websocket/lib/Message/Ping.php
  89. 15 0
      vendor/textalk/websocket/lib/Message/Pong.php
  90. 15 0
      vendor/textalk/websocket/lib/Message/Text.php
  91. 22 0
      vendor/textalk/websocket/lib/OpcodeTrait.php
  92. 470 0
      vendor/textalk/websocket/lib/Server.php
  93. 14 0
      vendor/textalk/websocket/lib/TimeoutException.php
  94. 13 0
      vendor/textalk/websocket/phpunit.xml.dist
  95. 568 0
      vendor/textalk/websocket/tests/ClientTest.php
  96. 51 0
      vendor/textalk/websocket/tests/ExceptionTest.php
  97. 60 0
      vendor/textalk/websocket/tests/MessageTest.php
  98. 28 0
      vendor/textalk/websocket/tests/README.md
  99. 511 0
      vendor/textalk/websocket/tests/ServerTest.php
  100. 6 0
      vendor/textalk/websocket/tests/bootstrap.php

+ 5 - 0
composer.json

@@ -0,0 +1,5 @@
+{
+    "require": {
+        "textalk/websocket": "^1.6"
+    }
+}

+ 370 - 0
composer.lock

@@ -0,0 +1,370 @@
+{
+    "_readme": [
+        "This file locks the dependencies of your project to a known state",
+        "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+        "This file is @generated automatically"
+    ],
+    "content-hash": "e0e5824b81123a386c677a2f30ab8734",
+    "packages": [
+        {
+            "name": "phrity/net-uri",
+            "version": "1.3.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sirn-se/phrity-net-uri.git",
+                "reference": "3f458e0c4d1ddc0e218d7a5b9420127c63925f43"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sirn-se/phrity-net-uri/zipball/3f458e0c4d1ddc0e218d7a5b9420127c63925f43",
+                "reference": "3f458e0c4d1ddc0e218d7a5b9420127c63925f43",
+                "shasum": "",
+                "mirrors": [
+                    {
+                        "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                        "preferred": true
+                    }
+                ]
+            },
+            "require": {
+                "php": "^7.4 | ^8.0",
+                "psr/http-factory": "^1.0",
+                "psr/http-message": "^1.0 | ^2.0"
+            },
+            "require-dev": {
+                "php-coveralls/php-coveralls": "^2.0",
+                "phpunit/phpunit": "^9.0 | ^10.0",
+                "squizlabs/php_codesniffer": "^3.0"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Phrity\\Net\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Sören Jensen",
+                    "email": "sirn@sirn.se",
+                    "homepage": "https://phrity.sirn.se"
+                }
+            ],
+            "description": "PSR-7 Uri and PSR-17 UriFactory implementation",
+            "homepage": "https://phrity.sirn.se/net-uri",
+            "keywords": [
+                "psr-17",
+                "psr-7",
+                "uri",
+                "uri factory"
+            ],
+            "support": {
+                "issues": "https://github.com/sirn-se/phrity-net-uri/issues",
+                "source": "https://github.com/sirn-se/phrity-net-uri/tree/1.3.0"
+            },
+            "time": "2023-08-21T10:33:06+00:00"
+        },
+        {
+            "name": "phrity/util-errorhandler",
+            "version": "1.1.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sirn-se/phrity-util-errorhandler.git",
+                "reference": "483228156e06673963902b1cc1e6bd9541ab4d5e"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sirn-se/phrity-util-errorhandler/zipball/483228156e06673963902b1cc1e6bd9541ab4d5e",
+                "reference": "483228156e06673963902b1cc1e6bd9541ab4d5e",
+                "shasum": "",
+                "mirrors": [
+                    {
+                        "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                        "preferred": true
+                    }
+                ]
+            },
+            "require": {
+                "php": "^7.4 | ^8.0"
+            },
+            "require-dev": {
+                "php-coveralls/php-coveralls": "^2.0",
+                "phpunit/phpunit": "^9.0 | ^10.0 | ^11.0",
+                "squizlabs/php_codesniffer": "^3.5"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Phrity\\Util\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Sören Jensen",
+                    "email": "sirn@sirn.se",
+                    "homepage": "https://phrity.sirn.se"
+                }
+            ],
+            "description": "Inline error handler; catch and resolve errors for code block.",
+            "homepage": "https://phrity.sirn.se/util-errorhandler",
+            "keywords": [
+                "error",
+                "warning"
+            ],
+            "support": {
+                "issues": "https://github.com/sirn-se/phrity-util-errorhandler/issues",
+                "source": "https://github.com/sirn-se/phrity-util-errorhandler/tree/1.1.1"
+            },
+            "time": "2024-09-12T06:49:16+00:00"
+        },
+        {
+            "name": "psr/http-factory",
+            "version": "1.0.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/http-factory.git",
+                "reference": "e616d01114759c4c489f93b099585439f795fe35"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/http-factory/zipball/e616d01114759c4c489f93b099585439f795fe35",
+                "reference": "e616d01114759c4c489f93b099585439f795fe35",
+                "shasum": "",
+                "mirrors": [
+                    {
+                        "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                        "preferred": true
+                    }
+                ]
+            },
+            "require": {
+                "php": ">=7.0.0",
+                "psr/http-message": "^1.0 || ^2.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Http\\Message\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "https://www.php-fig.org/"
+                }
+            ],
+            "description": "Common interfaces for PSR-7 HTTP message factories",
+            "keywords": [
+                "factory",
+                "http",
+                "message",
+                "psr",
+                "psr-17",
+                "psr-7",
+                "request",
+                "response"
+            ],
+            "support": {
+                "source": "https://github.com/php-fig/http-factory/tree/1.0.2"
+            },
+            "time": "2023-04-10T20:10:41+00:00"
+        },
+        {
+            "name": "psr/http-message",
+            "version": "1.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/http-message.git",
+                "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba",
+                "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba",
+                "shasum": "",
+                "mirrors": [
+                    {
+                        "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                        "preferred": true
+                    }
+                ]
+            },
+            "require": {
+                "php": "^7.2 || ^8.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.1.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Http\\Message\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "http://www.php-fig.org/"
+                }
+            ],
+            "description": "Common interface for HTTP messages",
+            "homepage": "https://github.com/php-fig/http-message",
+            "keywords": [
+                "http",
+                "http-message",
+                "psr",
+                "psr-7",
+                "request",
+                "response"
+            ],
+            "support": {
+                "source": "https://github.com/php-fig/http-message/tree/1.1"
+            },
+            "time": "2023-04-04T09:50:52+00:00"
+        },
+        {
+            "name": "psr/log",
+            "version": "3.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/log.git",
+                "reference": "79dff0b268932c640297f5208d6298f71855c03e"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/log/zipball/79dff0b268932c640297f5208d6298f71855c03e",
+                "reference": "79dff0b268932c640297f5208d6298f71855c03e",
+                "shasum": "",
+                "mirrors": [
+                    {
+                        "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                        "preferred": true
+                    }
+                ]
+            },
+            "require": {
+                "php": ">=8.0.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Log\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "https://www.php-fig.org/"
+                }
+            ],
+            "description": "Common interface for logging libraries",
+            "homepage": "https://github.com/php-fig/log",
+            "keywords": [
+                "log",
+                "psr",
+                "psr-3"
+            ],
+            "support": {
+                "source": "https://github.com/php-fig/log/tree/3.0.1"
+            },
+            "time": "2024-08-21T13:31:24+00:00"
+        },
+        {
+            "name": "textalk/websocket",
+            "version": "1.6.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/Textalk/websocket-php.git",
+                "reference": "67de79745b1a357caf812bfc44e0abf481cee012"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/Textalk/websocket-php/zipball/67de79745b1a357caf812bfc44e0abf481cee012",
+                "reference": "67de79745b1a357caf812bfc44e0abf481cee012",
+                "shasum": "",
+                "mirrors": [
+                    {
+                        "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                        "preferred": true
+                    }
+                ]
+            },
+            "require": {
+                "php": "^7.4 | ^8.0",
+                "phrity/net-uri": "^1.0",
+                "phrity/util-errorhandler": "^1.0",
+                "psr/http-message": "^1.0",
+                "psr/log": "^1.0 | ^2.0 | ^3.0"
+            },
+            "require-dev": {
+                "php-coveralls/php-coveralls": "^2.0",
+                "phpunit/phpunit": "^9.0",
+                "squizlabs/php_codesniffer": "^3.5"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "WebSocket\\": "lib"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "ISC"
+            ],
+            "authors": [
+                {
+                    "name": "Fredrik Liljegren"
+                },
+                {
+                    "name": "Sören Jensen"
+                }
+            ],
+            "description": "WebSocket client and server",
+            "support": {
+                "issues": "https://github.com/Textalk/websocket-php/issues",
+                "source": "https://github.com/Textalk/websocket-php/tree/1.6.3"
+            },
+            "time": "2022-11-07T18:59:33+00:00"
+        }
+    ],
+    "packages-dev": [],
+    "aliases": [],
+    "minimum-stability": "stable",
+    "stability-flags": [],
+    "prefer-stable": false,
+    "prefer-lowest": false,
+    "platform": [],
+    "platform-dev": [],
+    "plugin-api-version": "2.6.0"
+}

+ 25 - 0
vendor/autoload.php

@@ -0,0 +1,25 @@
+<?php
+
+// autoload.php @generated by Composer
+
+if (PHP_VERSION_ID < 50600) {
+    if (!headers_sent()) {
+        header('HTTP/1.1 500 Internal Server Error');
+    }
+    $err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
+    if (!ini_get('display_errors')) {
+        if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
+            fwrite(STDERR, $err);
+        } elseif (!headers_sent()) {
+            echo $err;
+        }
+    }
+    trigger_error(
+        $err,
+        E_USER_ERROR
+    );
+}
+
+require_once __DIR__ . '/composer/autoload_real.php';
+
+return ComposerAutoloaderInitdf1f81a0630f57ab5fb14da5bc0832c6::getLoader();

+ 579 - 0
vendor/composer/ClassLoader.php

@@ -0,0 +1,579 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Autoload;
+
+/**
+ * ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
+ *
+ *     $loader = new \Composer\Autoload\ClassLoader();
+ *
+ *     // register classes with namespaces
+ *     $loader->add('Symfony\Component', __DIR__.'/component');
+ *     $loader->add('Symfony',           __DIR__.'/framework');
+ *
+ *     // activate the autoloader
+ *     $loader->register();
+ *
+ *     // to enable searching the include path (eg. for PEAR packages)
+ *     $loader->setUseIncludePath(true);
+ *
+ * In this example, if you try to use a class in the Symfony\Component
+ * namespace or one of its children (Symfony\Component\Console for instance),
+ * the autoloader will first look for the class under the component/
+ * directory, and it will then fallback to the framework/ directory if not
+ * found before giving up.
+ *
+ * This class is loosely based on the Symfony UniversalClassLoader.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ * @see    https://www.php-fig.org/psr/psr-0/
+ * @see    https://www.php-fig.org/psr/psr-4/
+ */
+class ClassLoader
+{
+    /** @var \Closure(string):void */
+    private static $includeFile;
+
+    /** @var string|null */
+    private $vendorDir;
+
+    // PSR-4
+    /**
+     * @var array<string, array<string, int>>
+     */
+    private $prefixLengthsPsr4 = array();
+    /**
+     * @var array<string, list<string>>
+     */
+    private $prefixDirsPsr4 = array();
+    /**
+     * @var list<string>
+     */
+    private $fallbackDirsPsr4 = array();
+
+    // PSR-0
+    /**
+     * List of PSR-0 prefixes
+     *
+     * Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
+     *
+     * @var array<string, array<string, list<string>>>
+     */
+    private $prefixesPsr0 = array();
+    /**
+     * @var list<string>
+     */
+    private $fallbackDirsPsr0 = array();
+
+    /** @var bool */
+    private $useIncludePath = false;
+
+    /**
+     * @var array<string, string>
+     */
+    private $classMap = array();
+
+    /** @var bool */
+    private $classMapAuthoritative = false;
+
+    /**
+     * @var array<string, bool>
+     */
+    private $missingClasses = array();
+
+    /** @var string|null */
+    private $apcuPrefix;
+
+    /**
+     * @var array<string, self>
+     */
+    private static $registeredLoaders = array();
+
+    /**
+     * @param string|null $vendorDir
+     */
+    public function __construct($vendorDir = null)
+    {
+        $this->vendorDir = $vendorDir;
+        self::initializeIncludeClosure();
+    }
+
+    /**
+     * @return array<string, list<string>>
+     */
+    public function getPrefixes()
+    {
+        if (!empty($this->prefixesPsr0)) {
+            return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
+        }
+
+        return array();
+    }
+
+    /**
+     * @return array<string, list<string>>
+     */
+    public function getPrefixesPsr4()
+    {
+        return $this->prefixDirsPsr4;
+    }
+
+    /**
+     * @return list<string>
+     */
+    public function getFallbackDirs()
+    {
+        return $this->fallbackDirsPsr0;
+    }
+
+    /**
+     * @return list<string>
+     */
+    public function getFallbackDirsPsr4()
+    {
+        return $this->fallbackDirsPsr4;
+    }
+
+    /**
+     * @return array<string, string> Array of classname => path
+     */
+    public function getClassMap()
+    {
+        return $this->classMap;
+    }
+
+    /**
+     * @param array<string, string> $classMap Class to filename map
+     *
+     * @return void
+     */
+    public function addClassMap(array $classMap)
+    {
+        if ($this->classMap) {
+            $this->classMap = array_merge($this->classMap, $classMap);
+        } else {
+            $this->classMap = $classMap;
+        }
+    }
+
+    /**
+     * Registers a set of PSR-0 directories for a given prefix, either
+     * appending or prepending to the ones previously set for this prefix.
+     *
+     * @param string              $prefix  The prefix
+     * @param list<string>|string $paths   The PSR-0 root directories
+     * @param bool                $prepend Whether to prepend the directories
+     *
+     * @return void
+     */
+    public function add($prefix, $paths, $prepend = false)
+    {
+        $paths = (array) $paths;
+        if (!$prefix) {
+            if ($prepend) {
+                $this->fallbackDirsPsr0 = array_merge(
+                    $paths,
+                    $this->fallbackDirsPsr0
+                );
+            } else {
+                $this->fallbackDirsPsr0 = array_merge(
+                    $this->fallbackDirsPsr0,
+                    $paths
+                );
+            }
+
+            return;
+        }
+
+        $first = $prefix[0];
+        if (!isset($this->prefixesPsr0[$first][$prefix])) {
+            $this->prefixesPsr0[$first][$prefix] = $paths;
+
+            return;
+        }
+        if ($prepend) {
+            $this->prefixesPsr0[$first][$prefix] = array_merge(
+                $paths,
+                $this->prefixesPsr0[$first][$prefix]
+            );
+        } else {
+            $this->prefixesPsr0[$first][$prefix] = array_merge(
+                $this->prefixesPsr0[$first][$prefix],
+                $paths
+            );
+        }
+    }
+
+    /**
+     * Registers a set of PSR-4 directories for a given namespace, either
+     * appending or prepending to the ones previously set for this namespace.
+     *
+     * @param string              $prefix  The prefix/namespace, with trailing '\\'
+     * @param list<string>|string $paths   The PSR-4 base directories
+     * @param bool                $prepend Whether to prepend the directories
+     *
+     * @throws \InvalidArgumentException
+     *
+     * @return void
+     */
+    public function addPsr4($prefix, $paths, $prepend = false)
+    {
+        $paths = (array) $paths;
+        if (!$prefix) {
+            // Register directories for the root namespace.
+            if ($prepend) {
+                $this->fallbackDirsPsr4 = array_merge(
+                    $paths,
+                    $this->fallbackDirsPsr4
+                );
+            } else {
+                $this->fallbackDirsPsr4 = array_merge(
+                    $this->fallbackDirsPsr4,
+                    $paths
+                );
+            }
+        } elseif (!isset($this->prefixDirsPsr4[$prefix])) {
+            // Register directories for a new namespace.
+            $length = strlen($prefix);
+            if ('\\' !== $prefix[$length - 1]) {
+                throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
+            }
+            $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
+            $this->prefixDirsPsr4[$prefix] = $paths;
+        } elseif ($prepend) {
+            // Prepend directories for an already registered namespace.
+            $this->prefixDirsPsr4[$prefix] = array_merge(
+                $paths,
+                $this->prefixDirsPsr4[$prefix]
+            );
+        } else {
+            // Append directories for an already registered namespace.
+            $this->prefixDirsPsr4[$prefix] = array_merge(
+                $this->prefixDirsPsr4[$prefix],
+                $paths
+            );
+        }
+    }
+
+    /**
+     * Registers a set of PSR-0 directories for a given prefix,
+     * replacing any others previously set for this prefix.
+     *
+     * @param string              $prefix The prefix
+     * @param list<string>|string $paths  The PSR-0 base directories
+     *
+     * @return void
+     */
+    public function set($prefix, $paths)
+    {
+        if (!$prefix) {
+            $this->fallbackDirsPsr0 = (array) $paths;
+        } else {
+            $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
+        }
+    }
+
+    /**
+     * Registers a set of PSR-4 directories for a given namespace,
+     * replacing any others previously set for this namespace.
+     *
+     * @param string              $prefix The prefix/namespace, with trailing '\\'
+     * @param list<string>|string $paths  The PSR-4 base directories
+     *
+     * @throws \InvalidArgumentException
+     *
+     * @return void
+     */
+    public function setPsr4($prefix, $paths)
+    {
+        if (!$prefix) {
+            $this->fallbackDirsPsr4 = (array) $paths;
+        } else {
+            $length = strlen($prefix);
+            if ('\\' !== $prefix[$length - 1]) {
+                throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
+            }
+            $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
+            $this->prefixDirsPsr4[$prefix] = (array) $paths;
+        }
+    }
+
+    /**
+     * Turns on searching the include path for class files.
+     *
+     * @param bool $useIncludePath
+     *
+     * @return void
+     */
+    public function setUseIncludePath($useIncludePath)
+    {
+        $this->useIncludePath = $useIncludePath;
+    }
+
+    /**
+     * Can be used to check if the autoloader uses the include path to check
+     * for classes.
+     *
+     * @return bool
+     */
+    public function getUseIncludePath()
+    {
+        return $this->useIncludePath;
+    }
+
+    /**
+     * Turns off searching the prefix and fallback directories for classes
+     * that have not been registered with the class map.
+     *
+     * @param bool $classMapAuthoritative
+     *
+     * @return void
+     */
+    public function setClassMapAuthoritative($classMapAuthoritative)
+    {
+        $this->classMapAuthoritative = $classMapAuthoritative;
+    }
+
+    /**
+     * Should class lookup fail if not found in the current class map?
+     *
+     * @return bool
+     */
+    public function isClassMapAuthoritative()
+    {
+        return $this->classMapAuthoritative;
+    }
+
+    /**
+     * APCu prefix to use to cache found/not-found classes, if the extension is enabled.
+     *
+     * @param string|null $apcuPrefix
+     *
+     * @return void
+     */
+    public function setApcuPrefix($apcuPrefix)
+    {
+        $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
+    }
+
+    /**
+     * The APCu prefix in use, or null if APCu caching is not enabled.
+     *
+     * @return string|null
+     */
+    public function getApcuPrefix()
+    {
+        return $this->apcuPrefix;
+    }
+
+    /**
+     * Registers this instance as an autoloader.
+     *
+     * @param bool $prepend Whether to prepend the autoloader or not
+     *
+     * @return void
+     */
+    public function register($prepend = false)
+    {
+        spl_autoload_register(array($this, 'loadClass'), true, $prepend);
+
+        if (null === $this->vendorDir) {
+            return;
+        }
+
+        if ($prepend) {
+            self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
+        } else {
+            unset(self::$registeredLoaders[$this->vendorDir]);
+            self::$registeredLoaders[$this->vendorDir] = $this;
+        }
+    }
+
+    /**
+     * Unregisters this instance as an autoloader.
+     *
+     * @return void
+     */
+    public function unregister()
+    {
+        spl_autoload_unregister(array($this, 'loadClass'));
+
+        if (null !== $this->vendorDir) {
+            unset(self::$registeredLoaders[$this->vendorDir]);
+        }
+    }
+
+    /**
+     * Loads the given class or interface.
+     *
+     * @param  string    $class The name of the class
+     * @return true|null True if loaded, null otherwise
+     */
+    public function loadClass($class)
+    {
+        if ($file = $this->findFile($class)) {
+            $includeFile = self::$includeFile;
+            $includeFile($file);
+
+            return true;
+        }
+
+        return null;
+    }
+
+    /**
+     * Finds the path to the file where the class is defined.
+     *
+     * @param string $class The name of the class
+     *
+     * @return string|false The path if found, false otherwise
+     */
+    public function findFile($class)
+    {
+        // class map lookup
+        if (isset($this->classMap[$class])) {
+            return $this->classMap[$class];
+        }
+        if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
+            return false;
+        }
+        if (null !== $this->apcuPrefix) {
+            $file = apcu_fetch($this->apcuPrefix.$class, $hit);
+            if ($hit) {
+                return $file;
+            }
+        }
+
+        $file = $this->findFileWithExtension($class, '.php');
+
+        // Search for Hack files if we are running on HHVM
+        if (false === $file && defined('HHVM_VERSION')) {
+            $file = $this->findFileWithExtension($class, '.hh');
+        }
+
+        if (null !== $this->apcuPrefix) {
+            apcu_add($this->apcuPrefix.$class, $file);
+        }
+
+        if (false === $file) {
+            // Remember that this class does not exist.
+            $this->missingClasses[$class] = true;
+        }
+
+        return $file;
+    }
+
+    /**
+     * Returns the currently registered loaders keyed by their corresponding vendor directories.
+     *
+     * @return array<string, self>
+     */
+    public static function getRegisteredLoaders()
+    {
+        return self::$registeredLoaders;
+    }
+
+    /**
+     * @param  string       $class
+     * @param  string       $ext
+     * @return string|false
+     */
+    private function findFileWithExtension($class, $ext)
+    {
+        // PSR-4 lookup
+        $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
+
+        $first = $class[0];
+        if (isset($this->prefixLengthsPsr4[$first])) {
+            $subPath = $class;
+            while (false !== $lastPos = strrpos($subPath, '\\')) {
+                $subPath = substr($subPath, 0, $lastPos);
+                $search = $subPath . '\\';
+                if (isset($this->prefixDirsPsr4[$search])) {
+                    $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
+                    foreach ($this->prefixDirsPsr4[$search] as $dir) {
+                        if (file_exists($file = $dir . $pathEnd)) {
+                            return $file;
+                        }
+                    }
+                }
+            }
+        }
+
+        // PSR-4 fallback dirs
+        foreach ($this->fallbackDirsPsr4 as $dir) {
+            if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
+                return $file;
+            }
+        }
+
+        // PSR-0 lookup
+        if (false !== $pos = strrpos($class, '\\')) {
+            // namespaced class name
+            $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
+                . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
+        } else {
+            // PEAR-like class name
+            $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
+        }
+
+        if (isset($this->prefixesPsr0[$first])) {
+            foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
+                if (0 === strpos($class, $prefix)) {
+                    foreach ($dirs as $dir) {
+                        if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
+                            return $file;
+                        }
+                    }
+                }
+            }
+        }
+
+        // PSR-0 fallback dirs
+        foreach ($this->fallbackDirsPsr0 as $dir) {
+            if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
+                return $file;
+            }
+        }
+
+        // PSR-0 include paths.
+        if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
+            return $file;
+        }
+
+        return false;
+    }
+
+    /**
+     * @return void
+     */
+    private static function initializeIncludeClosure()
+    {
+        if (self::$includeFile !== null) {
+            return;
+        }
+
+        /**
+         * Scope isolated include.
+         *
+         * Prevents access to $this/self from included files.
+         *
+         * @param  string $file
+         * @return void
+         */
+        self::$includeFile = \Closure::bind(static function($file) {
+            include $file;
+        }, null, null);
+    }
+}

+ 359 - 0
vendor/composer/InstalledVersions.php

@@ -0,0 +1,359 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer;
+
+use Composer\Autoload\ClassLoader;
+use Composer\Semver\VersionParser;
+
+/**
+ * This class is copied in every Composer installed project and available to all
+ *
+ * See also https://getcomposer.org/doc/07-runtime.md#installed-versions
+ *
+ * To require its presence, you can require `composer-runtime-api ^2.0`
+ *
+ * @final
+ */
+class InstalledVersions
+{
+    /**
+     * @var mixed[]|null
+     * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
+     */
+    private static $installed;
+
+    /**
+     * @var bool|null
+     */
+    private static $canGetVendors;
+
+    /**
+     * @var array[]
+     * @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
+     */
+    private static $installedByVendor = array();
+
+    /**
+     * Returns a list of all package names which are present, either by being installed, replaced or provided
+     *
+     * @return string[]
+     * @psalm-return list<string>
+     */
+    public static function getInstalledPackages()
+    {
+        $packages = array();
+        foreach (self::getInstalled() as $installed) {
+            $packages[] = array_keys($installed['versions']);
+        }
+
+        if (1 === \count($packages)) {
+            return $packages[0];
+        }
+
+        return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
+    }
+
+    /**
+     * Returns a list of all package names with a specific type e.g. 'library'
+     *
+     * @param  string   $type
+     * @return string[]
+     * @psalm-return list<string>
+     */
+    public static function getInstalledPackagesByType($type)
+    {
+        $packagesByType = array();
+
+        foreach (self::getInstalled() as $installed) {
+            foreach ($installed['versions'] as $name => $package) {
+                if (isset($package['type']) && $package['type'] === $type) {
+                    $packagesByType[] = $name;
+                }
+            }
+        }
+
+        return $packagesByType;
+    }
+
+    /**
+     * Checks whether the given package is installed
+     *
+     * This also returns true if the package name is provided or replaced by another package
+     *
+     * @param  string $packageName
+     * @param  bool   $includeDevRequirements
+     * @return bool
+     */
+    public static function isInstalled($packageName, $includeDevRequirements = true)
+    {
+        foreach (self::getInstalled() as $installed) {
+            if (isset($installed['versions'][$packageName])) {
+                return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Checks whether the given package satisfies a version constraint
+     *
+     * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
+     *
+     *   Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
+     *
+     * @param  VersionParser $parser      Install composer/semver to have access to this class and functionality
+     * @param  string        $packageName
+     * @param  string|null   $constraint  A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
+     * @return bool
+     */
+    public static function satisfies(VersionParser $parser, $packageName, $constraint)
+    {
+        $constraint = $parser->parseConstraints((string) $constraint);
+        $provided = $parser->parseConstraints(self::getVersionRanges($packageName));
+
+        return $provided->matches($constraint);
+    }
+
+    /**
+     * Returns a version constraint representing all the range(s) which are installed for a given package
+     *
+     * It is easier to use this via isInstalled() with the $constraint argument if you need to check
+     * whether a given version of a package is installed, and not just whether it exists
+     *
+     * @param  string $packageName
+     * @return string Version constraint usable with composer/semver
+     */
+    public static function getVersionRanges($packageName)
+    {
+        foreach (self::getInstalled() as $installed) {
+            if (!isset($installed['versions'][$packageName])) {
+                continue;
+            }
+
+            $ranges = array();
+            if (isset($installed['versions'][$packageName]['pretty_version'])) {
+                $ranges[] = $installed['versions'][$packageName]['pretty_version'];
+            }
+            if (array_key_exists('aliases', $installed['versions'][$packageName])) {
+                $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
+            }
+            if (array_key_exists('replaced', $installed['versions'][$packageName])) {
+                $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
+            }
+            if (array_key_exists('provided', $installed['versions'][$packageName])) {
+                $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
+            }
+
+            return implode(' || ', $ranges);
+        }
+
+        throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+    }
+
+    /**
+     * @param  string      $packageName
+     * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
+     */
+    public static function getVersion($packageName)
+    {
+        foreach (self::getInstalled() as $installed) {
+            if (!isset($installed['versions'][$packageName])) {
+                continue;
+            }
+
+            if (!isset($installed['versions'][$packageName]['version'])) {
+                return null;
+            }
+
+            return $installed['versions'][$packageName]['version'];
+        }
+
+        throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+    }
+
+    /**
+     * @param  string      $packageName
+     * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
+     */
+    public static function getPrettyVersion($packageName)
+    {
+        foreach (self::getInstalled() as $installed) {
+            if (!isset($installed['versions'][$packageName])) {
+                continue;
+            }
+
+            if (!isset($installed['versions'][$packageName]['pretty_version'])) {
+                return null;
+            }
+
+            return $installed['versions'][$packageName]['pretty_version'];
+        }
+
+        throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+    }
+
+    /**
+     * @param  string      $packageName
+     * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
+     */
+    public static function getReference($packageName)
+    {
+        foreach (self::getInstalled() as $installed) {
+            if (!isset($installed['versions'][$packageName])) {
+                continue;
+            }
+
+            if (!isset($installed['versions'][$packageName]['reference'])) {
+                return null;
+            }
+
+            return $installed['versions'][$packageName]['reference'];
+        }
+
+        throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+    }
+
+    /**
+     * @param  string      $packageName
+     * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
+     */
+    public static function getInstallPath($packageName)
+    {
+        foreach (self::getInstalled() as $installed) {
+            if (!isset($installed['versions'][$packageName])) {
+                continue;
+            }
+
+            return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
+        }
+
+        throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+    }
+
+    /**
+     * @return array
+     * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
+     */
+    public static function getRootPackage()
+    {
+        $installed = self::getInstalled();
+
+        return $installed[0]['root'];
+    }
+
+    /**
+     * Returns the raw installed.php data for custom implementations
+     *
+     * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
+     * @return array[]
+     * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
+     */
+    public static function getRawData()
+    {
+        @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
+
+        if (null === self::$installed) {
+            // only require the installed.php file if this file is loaded from its dumped location,
+            // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
+            if (substr(__DIR__, -8, 1) !== 'C') {
+                self::$installed = include __DIR__ . '/installed.php';
+            } else {
+                self::$installed = array();
+            }
+        }
+
+        return self::$installed;
+    }
+
+    /**
+     * Returns the raw data of all installed.php which are currently loaded for custom implementations
+     *
+     * @return array[]
+     * @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
+     */
+    public static function getAllRawData()
+    {
+        return self::getInstalled();
+    }
+
+    /**
+     * Lets you reload the static array from another file
+     *
+     * This is only useful for complex integrations in which a project needs to use
+     * this class but then also needs to execute another project's autoloader in process,
+     * and wants to ensure both projects have access to their version of installed.php.
+     *
+     * A typical case would be PHPUnit, where it would need to make sure it reads all
+     * the data it needs from this class, then call reload() with
+     * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
+     * the project in which it runs can then also use this class safely, without
+     * interference between PHPUnit's dependencies and the project's dependencies.
+     *
+     * @param  array[] $data A vendor/composer/installed.php data set
+     * @return void
+     *
+     * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
+     */
+    public static function reload($data)
+    {
+        self::$installed = $data;
+        self::$installedByVendor = array();
+    }
+
+    /**
+     * @return array[]
+     * @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
+     */
+    private static function getInstalled()
+    {
+        if (null === self::$canGetVendors) {
+            self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
+        }
+
+        $installed = array();
+
+        if (self::$canGetVendors) {
+            foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
+                if (isset(self::$installedByVendor[$vendorDir])) {
+                    $installed[] = self::$installedByVendor[$vendorDir];
+                } elseif (is_file($vendorDir.'/composer/installed.php')) {
+                    /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
+                    $required = require $vendorDir.'/composer/installed.php';
+                    $installed[] = self::$installedByVendor[$vendorDir] = $required;
+                    if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
+                        self::$installed = $installed[count($installed) - 1];
+                    }
+                }
+            }
+        }
+
+        if (null === self::$installed) {
+            // only require the installed.php file if this file is loaded from its dumped location,
+            // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
+            if (substr(__DIR__, -8, 1) !== 'C') {
+                /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
+                $required = require __DIR__ . '/installed.php';
+                self::$installed = $required;
+            } else {
+                self::$installed = array();
+            }
+        }
+
+        if (self::$installed !== array()) {
+            $installed[] = self::$installed;
+        }
+
+        return $installed;
+    }
+}

+ 21 - 0
vendor/composer/LICENSE

@@ -0,0 +1,21 @@
+
+Copyright (c) Nils Adermann, Jordi Boggiano
+
+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.
+

+ 10 - 0
vendor/composer/autoload_classmap.php

@@ -0,0 +1,10 @@
+<?php
+
+// autoload_classmap.php @generated by Composer
+
+$vendorDir = dirname(__DIR__);
+$baseDir = dirname($vendorDir);
+
+return array(
+    'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
+);

+ 9 - 0
vendor/composer/autoload_namespaces.php

@@ -0,0 +1,9 @@
+<?php
+
+// autoload_namespaces.php @generated by Composer
+
+$vendorDir = dirname(__DIR__);
+$baseDir = dirname($vendorDir);
+
+return array(
+);

+ 14 - 0
vendor/composer/autoload_psr4.php

@@ -0,0 +1,14 @@
+<?php
+
+// autoload_psr4.php @generated by Composer
+
+$vendorDir = dirname(__DIR__);
+$baseDir = dirname($vendorDir);
+
+return array(
+    'WebSocket\\' => array($vendorDir . '/textalk/websocket/lib'),
+    'Psr\\Log\\' => array($vendorDir . '/psr/log/src'),
+    'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-message/src', $vendorDir . '/psr/http-factory/src'),
+    'Phrity\\Util\\' => array($vendorDir . '/phrity/util-errorhandler/src'),
+    'Phrity\\Net\\' => array($vendorDir . '/phrity/net-uri/src'),
+);

+ 38 - 0
vendor/composer/autoload_real.php

@@ -0,0 +1,38 @@
+<?php
+
+// autoload_real.php @generated by Composer
+
+class ComposerAutoloaderInitdf1f81a0630f57ab5fb14da5bc0832c6
+{
+    private static $loader;
+
+    public static function loadClassLoader($class)
+    {
+        if ('Composer\Autoload\ClassLoader' === $class) {
+            require __DIR__ . '/ClassLoader.php';
+        }
+    }
+
+    /**
+     * @return \Composer\Autoload\ClassLoader
+     */
+    public static function getLoader()
+    {
+        if (null !== self::$loader) {
+            return self::$loader;
+        }
+
+        require __DIR__ . '/platform_check.php';
+
+        spl_autoload_register(array('ComposerAutoloaderInitdf1f81a0630f57ab5fb14da5bc0832c6', 'loadClassLoader'), true, true);
+        self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
+        spl_autoload_unregister(array('ComposerAutoloaderInitdf1f81a0630f57ab5fb14da5bc0832c6', 'loadClassLoader'));
+
+        require __DIR__ . '/autoload_static.php';
+        call_user_func(\Composer\Autoload\ComposerStaticInitdf1f81a0630f57ab5fb14da5bc0832c6::getInitializer($loader));
+
+        $loader->register(true);
+
+        return $loader;
+    }
+}

+ 60 - 0
vendor/composer/autoload_static.php

@@ -0,0 +1,60 @@
+<?php
+
+// autoload_static.php @generated by Composer
+
+namespace Composer\Autoload;
+
+class ComposerStaticInitdf1f81a0630f57ab5fb14da5bc0832c6
+{
+    public static $prefixLengthsPsr4 = array (
+        'W' => 
+        array (
+            'WebSocket\\' => 10,
+        ),
+        'P' => 
+        array (
+            'Psr\\Log\\' => 8,
+            'Psr\\Http\\Message\\' => 17,
+            'Phrity\\Util\\' => 12,
+            'Phrity\\Net\\' => 11,
+        ),
+    );
+
+    public static $prefixDirsPsr4 = array (
+        'WebSocket\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/textalk/websocket/lib',
+        ),
+        'Psr\\Log\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/psr/log/src',
+        ),
+        'Psr\\Http\\Message\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/psr/http-message/src',
+            1 => __DIR__ . '/..' . '/psr/http-factory/src',
+        ),
+        'Phrity\\Util\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/phrity/util-errorhandler/src',
+        ),
+        'Phrity\\Net\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/phrity/net-uri/src',
+        ),
+    );
+
+    public static $classMap = array (
+        'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
+    );
+
+    public static function getInitializer(ClassLoader $loader)
+    {
+        return \Closure::bind(function () use ($loader) {
+            $loader->prefixLengthsPsr4 = ComposerStaticInitdf1f81a0630f57ab5fb14da5bc0832c6::$prefixLengthsPsr4;
+            $loader->prefixDirsPsr4 = ComposerStaticInitdf1f81a0630f57ab5fb14da5bc0832c6::$prefixDirsPsr4;
+            $loader->classMap = ComposerStaticInitdf1f81a0630f57ab5fb14da5bc0832c6::$classMap;
+
+        }, null, ClassLoader::class);
+    }
+}

+ 375 - 0
vendor/composer/installed.json

@@ -0,0 +1,375 @@
+{
+    "packages": [
+        {
+            "name": "phrity/net-uri",
+            "version": "1.3.0",
+            "version_normalized": "1.3.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sirn-se/phrity-net-uri.git",
+                "reference": "3f458e0c4d1ddc0e218d7a5b9420127c63925f43"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sirn-se/phrity-net-uri/zipball/3f458e0c4d1ddc0e218d7a5b9420127c63925f43",
+                "reference": "3f458e0c4d1ddc0e218d7a5b9420127c63925f43",
+                "shasum": "",
+                "mirrors": [
+                    {
+                        "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                        "preferred": true
+                    }
+                ]
+            },
+            "require": {
+                "php": "^7.4 | ^8.0",
+                "psr/http-factory": "^1.0",
+                "psr/http-message": "^1.0 | ^2.0"
+            },
+            "require-dev": {
+                "php-coveralls/php-coveralls": "^2.0",
+                "phpunit/phpunit": "^9.0 | ^10.0",
+                "squizlabs/php_codesniffer": "^3.0"
+            },
+            "time": "2023-08-21T10:33:06+00:00",
+            "type": "library",
+            "installation-source": "dist",
+            "autoload": {
+                "psr-4": {
+                    "Phrity\\Net\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Sören Jensen",
+                    "email": "sirn@sirn.se",
+                    "homepage": "https://phrity.sirn.se"
+                }
+            ],
+            "description": "PSR-7 Uri and PSR-17 UriFactory implementation",
+            "homepage": "https://phrity.sirn.se/net-uri",
+            "keywords": [
+                "psr-17",
+                "psr-7",
+                "uri",
+                "uri factory"
+            ],
+            "support": {
+                "issues": "https://github.com/sirn-se/phrity-net-uri/issues",
+                "source": "https://github.com/sirn-se/phrity-net-uri/tree/1.3.0"
+            },
+            "install-path": "../phrity/net-uri"
+        },
+        {
+            "name": "phrity/util-errorhandler",
+            "version": "1.1.1",
+            "version_normalized": "1.1.1.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sirn-se/phrity-util-errorhandler.git",
+                "reference": "483228156e06673963902b1cc1e6bd9541ab4d5e"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sirn-se/phrity-util-errorhandler/zipball/483228156e06673963902b1cc1e6bd9541ab4d5e",
+                "reference": "483228156e06673963902b1cc1e6bd9541ab4d5e",
+                "shasum": "",
+                "mirrors": [
+                    {
+                        "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                        "preferred": true
+                    }
+                ]
+            },
+            "require": {
+                "php": "^7.4 | ^8.0"
+            },
+            "require-dev": {
+                "php-coveralls/php-coveralls": "^2.0",
+                "phpunit/phpunit": "^9.0 | ^10.0 | ^11.0",
+                "squizlabs/php_codesniffer": "^3.5"
+            },
+            "time": "2024-09-12T06:49:16+00:00",
+            "type": "library",
+            "installation-source": "dist",
+            "autoload": {
+                "psr-4": {
+                    "Phrity\\Util\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Sören Jensen",
+                    "email": "sirn@sirn.se",
+                    "homepage": "https://phrity.sirn.se"
+                }
+            ],
+            "description": "Inline error handler; catch and resolve errors for code block.",
+            "homepage": "https://phrity.sirn.se/util-errorhandler",
+            "keywords": [
+                "error",
+                "warning"
+            ],
+            "support": {
+                "issues": "https://github.com/sirn-se/phrity-util-errorhandler/issues",
+                "source": "https://github.com/sirn-se/phrity-util-errorhandler/tree/1.1.1"
+            },
+            "install-path": "../phrity/util-errorhandler"
+        },
+        {
+            "name": "psr/http-factory",
+            "version": "1.0.2",
+            "version_normalized": "1.0.2.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/http-factory.git",
+                "reference": "e616d01114759c4c489f93b099585439f795fe35"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/http-factory/zipball/e616d01114759c4c489f93b099585439f795fe35",
+                "reference": "e616d01114759c4c489f93b099585439f795fe35",
+                "shasum": "",
+                "mirrors": [
+                    {
+                        "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                        "preferred": true
+                    }
+                ]
+            },
+            "require": {
+                "php": ">=7.0.0",
+                "psr/http-message": "^1.0 || ^2.0"
+            },
+            "time": "2023-04-10T20:10:41+00:00",
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0.x-dev"
+                }
+            },
+            "installation-source": "dist",
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Http\\Message\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "https://www.php-fig.org/"
+                }
+            ],
+            "description": "Common interfaces for PSR-7 HTTP message factories",
+            "keywords": [
+                "factory",
+                "http",
+                "message",
+                "psr",
+                "psr-17",
+                "psr-7",
+                "request",
+                "response"
+            ],
+            "support": {
+                "source": "https://github.com/php-fig/http-factory/tree/1.0.2"
+            },
+            "install-path": "../psr/http-factory"
+        },
+        {
+            "name": "psr/http-message",
+            "version": "1.1",
+            "version_normalized": "1.1.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/http-message.git",
+                "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba",
+                "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba",
+                "shasum": "",
+                "mirrors": [
+                    {
+                        "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                        "preferred": true
+                    }
+                ]
+            },
+            "require": {
+                "php": "^7.2 || ^8.0"
+            },
+            "time": "2023-04-04T09:50:52+00:00",
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.1.x-dev"
+                }
+            },
+            "installation-source": "dist",
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Http\\Message\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "http://www.php-fig.org/"
+                }
+            ],
+            "description": "Common interface for HTTP messages",
+            "homepage": "https://github.com/php-fig/http-message",
+            "keywords": [
+                "http",
+                "http-message",
+                "psr",
+                "psr-7",
+                "request",
+                "response"
+            ],
+            "support": {
+                "source": "https://github.com/php-fig/http-message/tree/1.1"
+            },
+            "install-path": "../psr/http-message"
+        },
+        {
+            "name": "psr/log",
+            "version": "3.0.1",
+            "version_normalized": "3.0.1.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/log.git",
+                "reference": "79dff0b268932c640297f5208d6298f71855c03e"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/log/zipball/79dff0b268932c640297f5208d6298f71855c03e",
+                "reference": "79dff0b268932c640297f5208d6298f71855c03e",
+                "shasum": "",
+                "mirrors": [
+                    {
+                        "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                        "preferred": true
+                    }
+                ]
+            },
+            "require": {
+                "php": ">=8.0.0"
+            },
+            "time": "2024-08-21T13:31:24+00:00",
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.x-dev"
+                }
+            },
+            "installation-source": "dist",
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Log\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "https://www.php-fig.org/"
+                }
+            ],
+            "description": "Common interface for logging libraries",
+            "homepage": "https://github.com/php-fig/log",
+            "keywords": [
+                "log",
+                "psr",
+                "psr-3"
+            ],
+            "support": {
+                "source": "https://github.com/php-fig/log/tree/3.0.1"
+            },
+            "install-path": "../psr/log"
+        },
+        {
+            "name": "textalk/websocket",
+            "version": "1.6.3",
+            "version_normalized": "1.6.3.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/Textalk/websocket-php.git",
+                "reference": "67de79745b1a357caf812bfc44e0abf481cee012"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/Textalk/websocket-php/zipball/67de79745b1a357caf812bfc44e0abf481cee012",
+                "reference": "67de79745b1a357caf812bfc44e0abf481cee012",
+                "shasum": "",
+                "mirrors": [
+                    {
+                        "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+                        "preferred": true
+                    }
+                ]
+            },
+            "require": {
+                "php": "^7.4 | ^8.0",
+                "phrity/net-uri": "^1.0",
+                "phrity/util-errorhandler": "^1.0",
+                "psr/http-message": "^1.0",
+                "psr/log": "^1.0 | ^2.0 | ^3.0"
+            },
+            "require-dev": {
+                "php-coveralls/php-coveralls": "^2.0",
+                "phpunit/phpunit": "^9.0",
+                "squizlabs/php_codesniffer": "^3.5"
+            },
+            "time": "2022-11-07T18:59:33+00:00",
+            "type": "library",
+            "installation-source": "dist",
+            "autoload": {
+                "psr-4": {
+                    "WebSocket\\": "lib"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "ISC"
+            ],
+            "authors": [
+                {
+                    "name": "Fredrik Liljegren"
+                },
+                {
+                    "name": "Sören Jensen"
+                }
+            ],
+            "description": "WebSocket client and server",
+            "support": {
+                "issues": "https://github.com/Textalk/websocket-php/issues",
+                "source": "https://github.com/Textalk/websocket-php/tree/1.6.3"
+            },
+            "install-path": "../textalk/websocket"
+        }
+    ],
+    "dev": true,
+    "dev-package-names": []
+}

+ 77 - 0
vendor/composer/installed.php

@@ -0,0 +1,77 @@
+<?php return array(
+    'root' => array(
+        'name' => '__root__',
+        'pretty_version' => 'dev-master',
+        'version' => 'dev-master',
+        'reference' => 'ab3edb0bdb599da2831e8de42f494eae47bdb8bf',
+        'type' => 'library',
+        'install_path' => __DIR__ . '/../../',
+        'aliases' => array(),
+        'dev' => true,
+    ),
+    'versions' => array(
+        '__root__' => array(
+            'pretty_version' => 'dev-master',
+            'version' => 'dev-master',
+            'reference' => 'ab3edb0bdb599da2831e8de42f494eae47bdb8bf',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../../',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+        'phrity/net-uri' => array(
+            'pretty_version' => '1.3.0',
+            'version' => '1.3.0.0',
+            'reference' => '3f458e0c4d1ddc0e218d7a5b9420127c63925f43',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../phrity/net-uri',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+        'phrity/util-errorhandler' => array(
+            'pretty_version' => '1.1.1',
+            'version' => '1.1.1.0',
+            'reference' => '483228156e06673963902b1cc1e6bd9541ab4d5e',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../phrity/util-errorhandler',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+        'psr/http-factory' => array(
+            'pretty_version' => '1.0.2',
+            'version' => '1.0.2.0',
+            'reference' => 'e616d01114759c4c489f93b099585439f795fe35',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../psr/http-factory',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+        'psr/http-message' => array(
+            'pretty_version' => '1.1',
+            'version' => '1.1.0.0',
+            'reference' => 'cb6ce4845ce34a8ad9e68117c10ee90a29919eba',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../psr/http-message',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+        'psr/log' => array(
+            'pretty_version' => '3.0.1',
+            'version' => '3.0.1.0',
+            'reference' => '79dff0b268932c640297f5208d6298f71855c03e',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../psr/log',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+        'textalk/websocket' => array(
+            'pretty_version' => '1.6.3',
+            'version' => '1.6.3.0',
+            'reference' => '67de79745b1a357caf812bfc44e0abf481cee012',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../textalk/websocket',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+    ),
+);

+ 26 - 0
vendor/composer/platform_check.php

@@ -0,0 +1,26 @@
+<?php
+
+// platform_check.php @generated by Composer
+
+$issues = array();
+
+if (!(PHP_VERSION_ID >= 80000)) {
+    $issues[] = 'Your Composer dependencies require a PHP version ">= 8.0.0". You are running ' . PHP_VERSION . '.';
+}
+
+if ($issues) {
+    if (!headers_sent()) {
+        header('HTTP/1.1 500 Internal Server Error');
+    }
+    if (!ini_get('display_errors')) {
+        if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
+            fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
+        } elseif (!headers_sent()) {
+            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
+    );
+}

+ 30 - 0
vendor/phrity/net-uri/composer.json

@@ -0,0 +1,30 @@
+{
+    "name": "phrity/net-uri",
+    "type": "library",
+    "description": "PSR-7 Uri and PSR-17 UriFactory implementation",
+    "homepage": "https://phrity.sirn.se/net-uri",
+    "keywords": ["uri", "uri factory", "PSR-7", "PSR-17"],
+    "license": "MIT",
+    "authors": [
+        {
+            "name": "Sören Jensen",
+            "email": "sirn@sirn.se",
+            "homepage": "https://phrity.sirn.se"
+        }
+    ],
+    "autoload": {
+        "psr-4": {
+            "Phrity\\Net\\": "src/"
+        }
+    },
+    "require": {
+        "php": "^7.4 | ^8.0",
+        "psr/http-factory": "^1.0",
+        "psr/http-message": "^1.0 | ^2.0"
+    },
+    "require-dev": {
+        "phpunit/phpunit": "^9.0 | ^10.0",
+        "php-coveralls/php-coveralls": "^2.0",
+        "squizlabs/php_codesniffer": "^3.0"
+    }
+}

+ 486 - 0
vendor/phrity/net-uri/src/Uri.php

@@ -0,0 +1,486 @@
+<?php
+
+/**
+ * File for Net\Uri class.
+ * @package Phrity > Net > Uri
+ * @see https://www.rfc-editor.org/rfc/rfc3986
+ * @see https://www.php-fig.org/psr/psr-7/#35-psrhttpmessageuriinterface
+ */
+
+namespace Phrity\Net;
+
+use InvalidArgumentException;
+use Psr\Http\Message\UriInterface;
+
+/**
+ * Net\Uri class.
+ */
+class Uri implements UriInterface
+{
+    public const REQUIRE_PORT = 1; // Always include port, explicit or default
+    public const ABSOLUTE_PATH = 2; // Enforce absolute path
+    public const NORMALIZE_PATH = 4; // Normalize path
+    public const IDNA = 8; // IDNA-convert host
+
+    private const RE_MAIN = '!^(?P<schemec>(?P<scheme>[^:/?#]+):)?(?P<authorityc>//(?P<authority>[^/?#]*))?'
+                          . '(?P<path>[^?#]*)(?P<queryc>\?(?P<query>[^#]*))?(?P<fragmentc>#(?P<fragment>.*))?$!';
+    private const RE_AUTH = '!^(?P<userinfoc>(?P<user>[^:/?#]+)(?P<passc>:(?P<pass>[^:/?#]+))?@)?'
+                          . '(?P<host>[^:/?#]*|\[[^/?#]*\])(?P<portc>:(?P<port>[0-9]*))?$!';
+
+    private static $port_defaults = [
+        'acap' => 674,
+        'afp' => 548,
+        'dict' => 2628,
+        'dns' => 53,
+        'ftp' => 21,
+        'git' => 9418,
+        'gopher' => 70,
+        'http' => 80,
+        'https' => 443,
+        'imap' => 143,
+        'ipp' => 631,
+        'ipps' => 631,
+        'irc' => 194,
+        'ircs' => 6697,
+        'ldap' => 389,
+        'ldaps' => 636,
+        'mms' => 1755,
+        'msrp' => 2855,
+        'mtqp' => 1038,
+        'nfs' => 111,
+        'nntp' => 119,
+        'nntps' => 563,
+        'pop' => 110,
+        'prospero' => 1525,
+        'redis' => 6379,
+        'rsync' => 873,
+        'rtsp' => 554,
+        'rtsps' => 322,
+        'rtspu' => 5005,
+        'sftp' => 22,
+        'smb' => 445,
+        'snmp' => 161,
+        'ssh' => 22,
+        'svn' => 3690,
+        'telnet' => 23,
+        'ventrilo' => 3784,
+        'vnc' => 5900,
+        'wais' => 210,
+        'ws' => 80,
+        'wss' => 443,
+    ];
+
+    private $scheme;
+    private $authority;
+    private $host;
+    private $port;
+    private $user;
+    private $pass;
+    private $path;
+    private $query;
+    private $fragment;
+
+    /**
+     * Create new URI instance using a string
+     * @param string $uri_string URI as string
+     * @throws \InvalidArgumentException If the given URI cannot be parsed
+     */
+    public function __construct(string $uri_string = '', int $flags = 0)
+    {
+        $this->parse($uri_string);
+    }
+
+
+    // ---------- PSR-7 getters ---------------------------------------------------------------------------------------
+
+    /**
+     * Retrieve the scheme component of the URI.
+     * @return string The URI scheme
+     */
+    public function getScheme(int $flags = 0): string
+    {
+        return $this->getComponent('scheme') ?? '';
+    }
+
+    /**
+     * Retrieve the authority component of the URI.
+     * @return string The URI authority, in "[user-info@]host[:port]" format
+     */
+    public function getAuthority(int $flags = 0): string
+    {
+        $host = $this->formatComponent($this->getHost($flags));
+        if ($this->isEmpty($host)) {
+            return '';
+        }
+        $userinfo = $this->formatComponent($this->getUserInfo(), '', '@');
+        $port = $this->formatComponent($this->getPort($flags), ':');
+        return "{$userinfo}{$host}{$port}";
+    }
+
+    /**
+     * Retrieve the user information component of the URI.
+     * @return string The URI user information, in "username[:password]" format
+     */
+    public function getUserInfo(int $flags = 0): string
+    {
+        $user = $this->formatComponent($this->getComponent('user'));
+        $pass = $this->formatComponent($this->getComponent('pass'), ':');
+        return $this->isEmpty($user) ? '' : "{$user}{$pass}";
+    }
+
+    /**
+     * Retrieve the host component of the URI.
+     * @return string The URI host
+     */
+    public function getHost(int $flags = 0): string
+    {
+        $host = $this->getComponent('host') ?? '';
+        if ($flags & self::IDNA) {
+            $host = $this->idna($host);
+        }
+        return $host;
+    }
+
+    /**
+     * Retrieve the port component of the URI.
+     * @return null|int The URI port
+     */
+    public function getPort(int $flags = 0): ?int
+    {
+        $port = $this->getComponent('port');
+        $scheme = $this->getComponent('scheme');
+        $default = isset(self::$port_defaults[$scheme]) ? self::$port_defaults[$scheme] : null;
+        if ($flags & self::REQUIRE_PORT) {
+            return !$this->isEmpty($port) ? $port : $default;
+        }
+        return $this->isEmpty($port) || $port === $default ? null : $port;
+    }
+
+    /**
+     * Retrieve the path component of the URI.
+     * @return string The URI path
+     */
+    public function getPath(int $flags = 0): string
+    {
+        $path = $this->getComponent('path') ?? '';
+        if ($flags & self::NORMALIZE_PATH) {
+            $path = $this->normalizePath($path);
+        }
+        if ($flags & self::ABSOLUTE_PATH && substr($path, 0, 1) !== '/') {
+            $path = "/{$path}";
+        }
+        return $path;
+    }
+
+    /**
+     * Retrieve the query string of the URI.
+     * @return string The URI query string
+     */
+    public function getQuery(int $flags = 0): string
+    {
+        return $this->getComponent('query') ?? '';
+    }
+
+    /**
+     * Retrieve the fragment component of the URI.
+     * @return string The URI fragment
+     */
+    public function getFragment(int $flags = 0): string
+    {
+        return $this->getComponent('fragment') ?? '';
+    }
+
+
+    // ---------- PSR-7 setters ---------------------------------------------------------------------------------------
+
+    /**
+     * Return an instance with the specified scheme.
+     * @param string $scheme The scheme to use with the new instance
+     * @return static A new instance with the specified scheme
+     * @throws \InvalidArgumentException for invalid schemes
+     * @throws \InvalidArgumentException for unsupported schemes
+     */
+    public function withScheme($scheme, int $flags = 0): UriInterface
+    {
+        $clone = clone $this;
+        if ($flags & self::REQUIRE_PORT) {
+            $clone->setComponent('port', $this->getPort(self::REQUIRE_PORT));
+            $default = isset(self::$port_defaults[$scheme]) ? self::$port_defaults[$scheme] : null;
+        }
+        $clone->setComponent('scheme', $scheme);
+        return $clone;
+    }
+
+    /**
+     * Return an instance with the specified user information.
+     * @param string $user The user name to use for authority
+     * @param null|string $password The password associated with $user
+     * @return static A new instance with the specified user information
+     */
+    public function withUserInfo($user, $password = null, int $flags = 0): UriInterface
+    {
+        $clone = clone $this;
+        $clone->setComponent('user', $user);
+        $clone->setComponent('pass', $password);
+        return $clone;
+    }
+
+    /**
+     * Return an instance with the specified host.
+     * @param string $host The hostname to use with the new instance
+     * @return static A new instance with the specified host
+     * @throws \InvalidArgumentException for invalid hostnames
+     */
+    public function withHost($host, int $flags = 0): UriInterface
+    {
+        $clone = clone $this;
+        if ($flags & self::IDNA) {
+            $host = $this->idna($host);
+        }
+        $clone->setComponent('host', $host);
+        return $clone;
+    }
+
+    /**
+     * Return an instance with the specified port.
+     * @param null|int $port The port to use with the new instance
+     * @return static A new instance with the specified port
+     * @throws \InvalidArgumentException for invalid ports
+     */
+    public function withPort($port, int $flags = 0): UriInterface
+    {
+        $clone = clone $this;
+        $clone->setComponent('port', $port);
+        return $clone;
+    }
+
+    /**
+     * Return an instance with the specified path.
+     * @param string $path The path to use with the new instance
+     * @return static A new instance with the specified path
+     * @throws \InvalidArgumentException for invalid paths
+     */
+    public function withPath($path, int $flags = 0): UriInterface
+    {
+        $clone = clone $this;
+        if ($flags & self::NORMALIZE_PATH) {
+            $path = $this->normalizePath($path);
+        }
+        if ($flags & self::ABSOLUTE_PATH && substr($path, 0, 1) !== '/') {
+            $path = "/{$path}";
+        }
+        $clone->setComponent('path', $path);
+        return $clone;
+    }
+
+    /**
+     * Return an instance with the specified query string.
+     * @param string $query The query string to use with the new instance
+     * @return static A new instance with the specified query string
+     * @throws \InvalidArgumentException for invalid query strings
+     */
+    public function withQuery($query, int $flags = 0): UriInterface
+    {
+        $clone = clone $this;
+        $clone->setComponent('query', $query);
+        return $clone;
+    }
+
+    /**
+     * Return an instance with the specified URI fragment.
+     * @param string $fragment The fragment to use with the new instance
+     * @return static A new instance with the specified fragment
+     */
+    public function withFragment($fragment, int $flags = 0): UriInterface
+    {
+        $clone = clone $this;
+        $clone->setComponent('fragment', $fragment);
+        return $clone;
+    }
+
+
+    // ---------- PSR-7 string ----------------------------------------------------------------------------------------
+
+    /**
+     * Return the string representation as a URI reference.
+     * @return string
+     */
+    public function __toString(): string
+    {
+        return $this->toString();
+    }
+
+
+    // ---------- Extensions ------------------------------------------------------------------------------------------
+
+    /**
+     * Return the string representation as a URI reference.
+     * @return string
+     */
+    public function toString(int $flags = 0): string
+    {
+        $scheme = $this->formatComponent($this->getComponent('scheme'), '', ':');
+        $authority = $this->authority ? "//{$this->formatComponent($this->getAuthority($flags))}" : '';
+        $path_flags = ($this->authority && $this->path ? self::ABSOLUTE_PATH : 0) | $flags;
+        $path = $this->formatComponent($this->getPath($path_flags));
+        $query = $this->formatComponent($this->getComponent('query'), '?');
+        $fragment = $this->formatComponent($this->getComponent('fragment'), '#');
+        return "{$scheme}{$authority}{$path}{$query}{$fragment}";
+    }
+
+
+    // ---------- Private helper methods ------------------------------------------------------------------------------
+
+    private function parse(string $uri_string = ''): void
+    {
+        if ($uri_string === '') {
+            return;
+        }
+        preg_match(self::RE_MAIN, $uri_string, $main);
+        $this->authority = !empty($main['authorityc']);
+        $this->setComponent('scheme', isset($main['schemec']) ? $main['scheme'] : '');
+        $this->setComponent('path', isset($main['path']) ? $main['path'] : '');
+        $this->setComponent('query', isset($main['queryc']) ? $main['query'] : '');
+        $this->setComponent('fragment', isset($main['fragmentc']) ? $main['fragment'] : '');
+        if ($this->authority) {
+            preg_match(self::RE_AUTH, $main['authority'], $auth);
+            if (empty($auth) && $main['authority'] !== '') {
+                throw new InvalidArgumentException("Invalid 'authority'.");
+            }
+            if ($this->isEmpty($auth['host']) && !$this->isEmpty($auth['user'])) {
+                throw new InvalidArgumentException("Invalid 'authority'.");
+            }
+            $this->setComponent('user', isset($auth['user']) ? $auth['user'] : '');
+            $this->setComponent('pass', isset($auth['passc']) ? $auth['pass'] : '');
+            $this->setComponent('host', isset($auth['host']) ? $auth['host'] : '');
+            $this->setComponent('port', isset($auth['portc']) ? $auth['port'] : '');
+        }
+    }
+
+    private function encode(string $source, string $keep = ''): string
+    {
+        $exclude = "[^%\/:=&!\$'()*+,;@{$keep}]+";
+        $exp = "/(%{$exclude})|({$exclude})/";
+        return preg_replace_callback($exp, function ($matches) {
+            if ($e = preg_match('/^(%[0-9a-fA-F]{2})/', $matches[0], $m)) {
+                return substr($matches[0], 0, 3) . rawurlencode(substr($matches[0], 3));
+            } else {
+                return rawurlencode($matches[0]);
+            }
+        }, $source);
+    }
+
+    private function setComponent(string $component, $value): void
+    {
+        $value = $this->parseCompontent($component, $value);
+        $this->$component = $value;
+    }
+
+    private function parseCompontent(string $component, $value)
+    {
+        if ($this->isEmpty($value)) {
+            return null;
+        }
+        switch ($component) {
+            case 'scheme':
+                $this->assertString($component, $value);
+                $this->assertpattern($component, $value, '/^[a-z][a-z0-9-+.]*$/i');
+                return mb_strtolower($value);
+            case 'host': // IP-literal / IPv4address / reg-name
+                $this->assertString($component, $value);
+                $this->authority = $this->authority || !$this->isEmpty($value);
+                return mb_strtolower($value);
+            case 'port':
+                $this->assertInteger($component, $value);
+                if ($value < 0 || $value > 65535) {
+                    throw new InvalidArgumentException("Invalid port number");
+                }
+                return (int)$value;
+            case 'path':
+                $this->assertString($component, $value);
+                $value = $this->encode($value);
+                return $value;
+            case 'user':
+            case 'pass':
+            case 'query':
+            case 'fragment':
+                $this->assertString($component, $value);
+                $value = $this->encode($value, '?');
+                return $value;
+        }
+    }
+
+    private function getComponent(string $component)
+    {
+        return isset($this->$component) ? $this->$component : null;
+    }
+
+    private function formatComponent($value, string $before = '', string $after = ''): string
+    {
+        return $this->isEmpty($value) ? '' : "{$before}{$value}{$after}";
+    }
+
+    private function isEmpty($value): bool
+    {
+        return is_null($value) || $value === '';
+    }
+
+    private function assertString(string $component, $value): void
+    {
+        if (!is_string($value)) {
+            throw new InvalidArgumentException("Invalid '{$component}': Should be a string");
+        }
+    }
+
+    private function assertInteger(string $component, $value): void
+    {
+        if (!is_numeric($value) || intval($value) != $value) {
+            throw new InvalidArgumentException("Invalid '{$component}': Should be an integer");
+        }
+    }
+
+    private function assertPattern(string $component, string $value, string $pattern): void
+    {
+        if (preg_match($pattern, $value) == 0) {
+            throw new InvalidArgumentException("Invalid '{$component}': Should match {$pattern}");
+        }
+    }
+
+    private function normalizePath(string $path): string
+    {
+        $result = [];
+        preg_match_all('!([^/]*/|[^/]*$)!', $path, $items);
+        foreach ($items[0] as $item) {
+            switch ($item) {
+                case '':
+                case './':
+                case '.':
+                    break; // just skip
+                case '/':
+                    if (empty($result)) {
+                        array_push($result, $item); // add
+                    }
+                    break;
+                case '..':
+                case '../':
+                    if (empty($result) || end($result) == '../') {
+                        array_push($result, $item); // add
+                    } else {
+                        array_pop($result); // remove previous
+                    }
+                    break;
+                default:
+                    array_push($result, $item); // add
+            }
+        }
+        return implode('', $result);
+    }
+
+    private function idna(string $value): string
+    {
+        if ($value === '' || !is_callable('idn_to_ascii')) {
+            return $value; // Can't convert, but don't cause exception
+        }
+        return idn_to_ascii($value, IDNA_NONTRANSITIONAL_TO_ASCII, INTL_IDNA_VARIANT_UTS46);
+    }
+}

+ 31 - 0
vendor/phrity/net-uri/src/UriFactory.php

@@ -0,0 +1,31 @@
+<?php
+
+/**
+ * File for Net\UriFactory class.
+ * @package Phrity > Net > Uri
+ * @see https://www.rfc-editor.org/rfc/rfc3986
+ * @see https://www.php-fig.org/psr/psr-17/#26-urifactoryinterface
+ */
+
+namespace Phrity\Net;
+
+use Psr\Http\Message\{
+    UriFactoryInterface,
+    UriInterface
+};
+
+/**
+ * Net\UriFactory class.
+ */
+class UriFactory implements UriFactoryInterface
+{
+    /**
+     * Create a new URI.
+     * @param string $uri The URI to parse.
+     * @throws \InvalidArgumentException If the given URI cannot be parsed
+     */
+    public function createUri(string $uri = ''): UriInterface
+    {
+        return new Uri($uri);
+    }
+}

+ 93 - 0
vendor/phrity/util-errorhandler/.github/workflows/acceptance.yml

@@ -0,0 +1,93 @@
+name: Acceptance
+
+on: [push, pull_request]
+
+jobs:
+
+  test:
+    strategy:
+      matrix:
+        php-versions: ["7.4", "8.0", "8.1", "8.2", "8.3", "8.4"]
+    runs-on: ubuntu-latest
+    name: Unit test
+    steps:
+    - name: Checkout
+      uses: actions/checkout@v4
+    - name: Set up PHP
+      uses: shivammathur/setup-php@v2
+      with:
+        php-version: ${{ matrix.php-versions }}
+        coverage: none
+      env:
+        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+    - name: Get composer cache directory
+      id: composer-cache
+      run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
+    - name: Cache dependencies
+      uses: actions/cache@v4
+      with:
+        path: ${{ steps.composer-cache.outputs.dir }}
+        key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
+        restore-keys: ${{ runner.os }}-composer-
+    - name: Install dependencies
+      run: composer install --prefer-dist
+    - name: Test
+      run: vendor/bin/phpunit
+
+  cs-check:
+    runs-on: ubuntu-latest
+    name: Code standard
+    steps:
+    - name: Checkout
+      uses: actions/checkout@v4
+    - name: Set up PHP
+      uses: shivammathur/setup-php@v2
+      with:
+        php-version: "8.3"
+        coverage: none
+      env:
+        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+    - name: Get composer cache directory
+      id: composer-cache
+      run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
+    - name: Cache dependencies
+      uses: actions/cache@v4
+      with:
+        path: ${{ steps.composer-cache.outputs.dir }}
+        key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
+        restore-keys: ${{ runner.os }}-composer-
+    - name: Install dependencies
+      run: composer install --prefer-dist
+    - name: Code standard
+      run: vendor/bin/phpcs --standard=PSR1,PSR12 --encoding=UTF-8 --report=full --colors src tests
+
+  coverage:
+    runs-on: ubuntu-latest
+    name: Code coverage
+    steps:
+    - name: Checkout
+      uses: actions/checkout@v4
+    - name: Set up PHP
+      uses: shivammathur/setup-php@v2
+      with:
+        php-version: "8.3"
+        coverage: xdebug
+      env:
+        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+    - name: Get composer cache directory
+      id: composer-cache
+      run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
+    - name: Cache dependencies
+      uses: actions/cache@v4
+      with:
+        path: ${{ steps.composer-cache.outputs.dir }}
+        key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
+        restore-keys: ${{ runner.os }}-composer-
+    - name: Install dependencies
+      run: composer install --prefer-dist
+    - name: Code coverage build
+      run: XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-clover build/logs/clover.xml
+    - name: Code coverage upload
+      env:
+        COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+      run: vendor/bin/php-coveralls -v

+ 6 - 0
vendor/phrity/util-errorhandler/.gitignore

@@ -0,0 +1,6 @@
+.DS_Store
+.phpunit.result.cache
+build/
+composer.lock
+composer.phar
+vendor/

+ 41 - 0
vendor/phrity/util-errorhandler/Makefile

@@ -0,0 +1,41 @@
+# Default
+all: deps-install
+
+
+# DEPENDENCY MANAGEMENT
+
+# Updates dependencies according to lock file
+deps-install: composer.phar
+	./composer.phar --no-interaction install
+
+# Updates dependencies according to json file
+deps-update: composer.phar
+	./composer.phar self-update
+	./composer.phar --no-interaction update
+
+
+# TESTS AND REPORTS
+
+# Code standard check
+cs-check: composer.lock
+	./vendor/bin/phpcs --standard=PSR1,PSR12 --encoding=UTF-8 --report=full --colors src tests
+
+# Run tests
+test: composer.lock
+	./vendor/bin/phpunit
+
+# Run tests with clover coverage report
+coverage: composer.lock
+	XDEBUG_MODE=coverage ./vendor/bin/phpunit --coverage-clover build/logs/clover.xml
+	./vendor/bin/php-coveralls -v
+
+
+# INITIAL INSTALL
+
+# Ensures composer is installed
+composer.phar:
+	curl -sS https://getcomposer.org/installer | php
+
+# Ensures composer is installed and dependencies loaded
+composer.lock: composer.phar
+	./composer.phar --no-interaction install

+ 147 - 0
vendor/phrity/util-errorhandler/README.md

@@ -0,0 +1,147 @@
+[![Build Status](https://github.com/sirn-se/phrity-util-errorhandler/actions/workflows/acceptance.yml/badge.svg)](https://github.com/sirn-se/phrity-util-errorhandler/actions)
+[![Coverage Status](https://coveralls.io/repos/github/sirn-se/phrity-util-errorhandler/badge.svg?branch=main)](https://coveralls.io/github/sirn-se/phrity-util-errorhandler?branch=main)
+
+# Error Handler utility
+
+The PHP [error handling](https://www.php.net/manual/en/book.errorfunc.php) can be somewhat of a headache.
+Typically an application uses a system level [error handler](https://www.php.net/manual/en/function.set-error-handler.php) and/or suppressing errors using the `@` prefix.
+But those cases when your code need to act on triggered errors are more tricky.
+
+This library provides two convenience methods to handle errors on code blocks, either by throwing exceptions or running callback code when an error occurs.
+
+Current version supports PHP `^7.2|^8.0`.
+
+## Installation
+
+Install with [Composer](https://getcomposer.org/);
+```
+composer require phrity/util-errorhandler
+```
+
+## The Error Handler
+
+The class provides two main methods; `with()` and `withAll()`.
+The difference is that `with()` will act immediately on an error and abort further code execution, while `withAll()` will attempt to execute the entire code block before acting on errors that occurred.
+
+### Throwing ErrorException
+
+```php
+use Phrity\Util\ErrorHandler;
+
+$handler = new ErrorHandler();
+$result = $handler->with(function () {
+    // Code to execute
+    return $success_result;
+});
+$result = $handler->withAll(function () {
+    // Code to execute
+    return $success_result;
+});
+```
+The examples above will run the callback code, but if an error occurs it will throw an [ErrorException](https://www.php.net/manual/en/class.errorexception.php).
+Error message and severity will be that of the triggering error.
+* `with()` will throw immediately when occured
+* `withAll()` will throw when code is complete; if more than one error occurred, the first will be thrown
+
+### Throwing specified Throwable
+
+```php
+use Phrity\Util\ErrorHandler;
+
+$handler = new ErrorHandler();
+$result = $handler->with(function () {
+    // Code to execute
+    return $success_result;
+}, new RuntimeException('A specified error'));
+$result = $handler->withAll(function () {
+    // Code to execute
+    return $success_result;
+}, new RuntimeException('A specified error'));
+```
+The examples above will run the callback code, but if an error occurs it will throw provided Throwable.
+The thrown Throwable will have an [ErrorException](https://www.php.net/manual/en/class.errorexception.php) attached as `$previous`.
+* `with()` will throw immediately when occured
+* `withAll()` will throw when code is complete; if more than one error occurred, the first will be thrown
+
+### Using callback
+
+```php
+use Phrity\Util\ErrorHandler;
+
+$handler = new ErrorHandler();
+$result = $handler->with(function () {
+    // Code to execute
+    return $success_result;
+}, function (ErrorException $error) {
+    // Code to handle error
+    return $error_result;
+});
+$result = $handler->withAll(function () {
+    // Code to execute
+    return $success_result;
+}, function (array $errors, $success_result) {
+    // Code to handle errors
+    return $error_result;
+});
+```
+The examples above will run the callback code, but if an error occurs it will call the error callback as well.
+* `with()` will run the error callback immediately when error occured; error callback expects an ErrorException instance
+* `withAll()` will run the error callback when code is complete; error callback expects an array of ErrorException and the returned result of code callback
+
+### Filtering error types
+
+Both `with()` and `withAll()` accepts error level(s) as last parameter.
+```php
+use Phrity\Util\ErrorHandler;
+
+$handler = new ErrorHandler();
+$result = $handler->with(function () {
+    // Code to execute
+    return $success_result;
+}, null, E_USER_ERROR);
+$result = $handler->withAll(function () {
+    // Code to execute
+    return $success_result;
+}, null, E_USER_ERROR & E_USER_WARNING);
+```
+Any value or combination of values accepted by [set_error_handler](https://www.php.net/manual/en/function.set-error-handler.php) is usable.
+Default is `E_ALL`. [List of constants](https://www.php.net/manual/en/errorfunc.constants.php).
+
+### The global error handler
+
+The class also has global `set()` and `restore()` methods.
+
+```php
+use Phrity\Util\ErrorHandler;
+
+$handler = new ErrorHandler();
+$handler->set(); // Throws ErrorException on error
+$handler->set(new RuntimeException('A specified error')); // Throws provided Throwable on error
+$handler->set(function (ErrorException $error) {
+    // Code to handle errors
+    return $error_result;
+}); // Runs callback on error
+$handler->restore(); // Restores error handler
+```
+
+###  Class synopsis
+
+```php
+Phrity\Util\ErrorHandler {
+
+    /* Methods */
+    public __construct()
+
+    public with(callable $callback, mixed $handling = null, int $levels = E_ALL) : mixed
+    public withAll(callable $callback, mixed $handling = null, int $levels = E_ALL) : mixed
+    public set($handling = null, int $levels = E_ALL) : mixed
+    public restore() : bool
+}
+```
+
+## Versions
+
+| Version | PHP | |
+| --- | --- | --- |
+| `1.1` | `^7.4\|^8.0` | Some improvements |
+| `1.0` | `^7.2\|^8.0` | Initial version |

+ 33 - 0
vendor/phrity/util-errorhandler/composer.json

@@ -0,0 +1,33 @@
+{
+    "name": "phrity/util-errorhandler",
+    "type": "library",
+    "description": "Inline error handler; catch and resolve errors for code block.",
+    "homepage": "https://phrity.sirn.se/util-errorhandler",
+    "keywords": ["error", "warning"],
+    "license": "MIT",
+    "authors": [
+        {
+            "name": "Sören Jensen",
+            "email": "sirn@sirn.se",
+            "homepage": "https://phrity.sirn.se"
+        }
+    ],
+    "autoload": {
+        "psr-4": {
+            "Phrity\\Util\\": "src/"
+        }
+    },
+    "autoload-dev": {
+        "psr-4": {
+            "Phrity\\Util\\Tests\\": "tests/"
+        }
+    },
+    "require": {
+        "php": "^7.4 | ^8.0"
+    },
+    "require-dev": {
+        "phpunit/phpunit": "^9.0 | ^10.0 | ^11.0",
+        "php-coveralls/php-coveralls": "^2.0",
+        "squizlabs/php_codesniffer": "^3.5"
+    }
+}

+ 13 - 0
vendor/phrity/util-errorhandler/phpunit.xml.dist

@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" colors="true" bootstrap="vendor/autoload.php" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/11.0/phpunit.xsd">
+  <testsuites>
+    <testsuite name="Phrity Util/Accessor tests">
+      <directory>./tests/</directory>
+    </testsuite>
+  </testsuites>
+  <source>
+    <include>
+      <directory suffix=".php">./src/</directory>
+    </include>
+  </source>
+</phpunit>

+ 121 - 0
vendor/phrity/util-errorhandler/src/ErrorHandler.php

@@ -0,0 +1,121 @@
+<?php
+
+/**
+ * File for ErrorHandler utility class.
+ * @package Phrity > Util > ErrorHandler
+ */
+
+namespace Phrity\Util;
+
+use ErrorException;
+use Throwable;
+
+/**
+ * ErrorHandler utility class.
+ * Allows catching and resolving errors inline.
+ */
+class ErrorHandler
+{
+    /* ----------------- Public methods ---------------------------------------------- */
+
+    /**
+     * Set error handler to run until removed.
+     * @param mixed $handling
+     *   - If null, handler will throw ErrorException
+     *   - If Throwable $t, throw $t with ErrorException attached as previous
+     *   - If callable, will invoke callback with ErrorException as argument
+     * @param int $levels Error levels to catch, all errors by default
+     * @return mixed Previously registered error handler, if any
+     */
+    public function set($handling = null, int $levels = E_ALL)
+    {
+        return set_error_handler($this->getHandler($handling), $levels);
+    }
+
+    /**
+     * Remove error handler.
+     * @return bool True if removed
+     */
+    public function restore(): bool
+    {
+        return restore_error_handler();
+    }
+
+    /**
+     * Run code with error handling, breaks on first encountered error.
+     * @param callable $callback The code to run
+     * @param mixed $handling
+     *   - If null, handler will throw ErrorException
+     *   - If Throwable $t, throw $t with ErrorException attached as previous
+     *   - If callable, will invoke callback with ErrorException as argument
+     * @param int $levels Error levels to catch, all errors by default
+     * @return mixed Return what $callback returns, or what $handling retuns on error
+     */
+    public function with(callable $callback, $handling = null, int $levels = E_ALL)
+    {
+        $error = null;
+        $result = null;
+        try {
+            $this->set(null, $levels);
+            $result = $callback();
+        } catch (ErrorException $e) {
+            $error = $this->handle($handling, $e);
+        } finally {
+            $this->restore();
+        }
+        return $error ?? $result;
+    }
+
+    /**
+     * Run code with error handling, comletes code before handling errors
+     * @param callable $callback The code to run
+     * @param mixed $handling
+     *   - If null, handler will throw ErrorException
+     *   - If Throwable $t, throw $t with ErrorException attached as previous
+     *   - If callable, will invoke callback with ErrorException as argument
+     * @param int $levels Error levels to catch, all errors by default
+     * @return mixed Return what $callback returns, or what $handling retuns on error
+     */
+    public function withAll(callable $callback, $handling = null, int $levels = E_ALL)
+    {
+        $errors = [];
+        $this->set(function (ErrorException $e) use (&$errors) {
+            $errors[] = $e;
+        }, $levels);
+        $result = $callback();
+        $this->restore();
+        $error = empty($errors) ? null : $this->handle($handling, $errors, $result);
+        return $error ?? $result;
+    }
+
+
+    /* ----------------- Private helpers --------------------------------------------- */
+
+    // Get handler function
+    private function getHandler($handling)
+    {
+        return function ($severity, $message, $file, $line) use ($handling) {
+            $error = new ErrorException($message, 0, $severity, $file, $line);
+            $this->handle($handling, $error);
+        };
+    }
+
+    // Handle error according to $handlig type
+    private function handle($handling, $error, $result = null)
+    {
+        if (is_callable($handling)) {
+            return $handling($error, $result);
+        }
+        if (is_array($error)) {
+            $error = array_shift($error);
+        }
+        if ($handling instanceof Throwable) {
+            try {
+                throw $error;
+            } finally {
+                throw $handling;
+            }
+        }
+        throw $error;
+    }
+}

+ 303 - 0
vendor/phrity/util-errorhandler/tests/ErrorHandlerTest.php

@@ -0,0 +1,303 @@
+<?php
+
+/**
+ * File for ErrorHandler function tests.
+ * @package Phrity > Util > ErrorHandler
+ */
+
+declare(strict_types=1);
+
+namespace Phrity\Util;
+
+use ErrorException;
+use RuntimeException;
+use Phrity\Util\ErrorHandler;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * ErrorHandler test class.
+ */
+class ErrorHandlerTest extends TestCase
+{
+    /**
+     * Set up for all tests
+     */
+    public function setUp(): void
+    {
+        error_reporting(-1);
+    }
+
+    public function testSetNull(): void
+    {
+        $handler = new ErrorHandler();
+        $handler->set();
+
+        // Verify exception
+        try {
+            trigger_error('An error');
+        } catch (ErrorException $e) {
+            $this->assertEquals('An error', $e->getMessage());
+            $this->assertEquals(0, $e->getCode());
+            $this->assertEquals(E_USER_NOTICE, $e->getSeverity());
+            $this->assertNull($e->getPrevious());
+        }
+
+        // Restore handler
+        $this->assertTrue($handler->restore());
+    }
+
+    public function testSetThrowable(): void
+    {
+        $handler = new ErrorHandler();
+        $handler->set(new RuntimeException('A provided exception', 23));
+
+        // Verify exception
+        try {
+            trigger_error('An error');
+        } catch (RuntimeException $e) {
+            $this->assertEquals('A provided exception', $e->getMessage());
+            $this->assertEquals(23, $e->getCode());
+            $this->assertNotNull($e->getPrevious());
+            $prev = $e->getPrevious();
+            $this->assertEquals('An error', $prev->getMessage());
+            $this->assertEquals(0, $prev->getCode());
+            $this->assertEquals(E_USER_NOTICE, $prev->getSeverity());
+            $this->assertNull($prev->getPrevious());
+        }
+
+        // Restore handler
+        $this->assertTrue($handler->restore());
+    }
+
+    public function testSetCallback(): void
+    {
+        $handler = new ErrorHandler();
+        $result = null;
+        $handler->set(function ($error) use (&$result) {
+            $result = [
+                'message' => $error->getMessage(),
+                'code' => $error->getCode(),
+                'severity' => $error->getSeverity(),
+            ];
+        });
+
+        // Verify exception
+        trigger_error('An error');
+        $this->assertEquals([
+            'message' => 'An error',
+            'code' => 0,
+            'severity' => E_USER_NOTICE,
+        ], $result);
+
+        // Restore handler
+        $this->assertTrue($handler->restore());
+    }
+
+    public function testWithNull(): void
+    {
+        $handler = new ErrorHandler();
+        $check = false;
+
+        // No exception
+        $result = $handler->with(function () {
+            return 'Code success';
+        });
+        $this->assertEquals('Code success', $result);
+
+        // Verify exception
+        try {
+            $result = $handler->with(function () use (&$check) {
+                trigger_error('An error');
+                $check = true;
+                return 'Code success';
+            });
+        } catch (ErrorException $e) {
+            $this->assertEquals('An error', $e->getMessage());
+            $this->assertEquals(0, $e->getCode());
+            $this->assertEquals(E_USER_NOTICE, $e->getSeverity());
+            $this->assertNull($e->getPrevious());
+        }
+        $this->assertFalse($check);
+
+         // Verify that exception is thrown
+        $this->expectException('ErrorException');
+        $result = $handler->with(function () {
+            trigger_error('An error');
+            return 'Code success';
+        });
+    }
+
+    public function testWithThrowable(): void
+    {
+        $handler = new ErrorHandler();
+        $check = false;
+
+        // No exception
+        $result = $handler->with(function () {
+            return 'Code success';
+        });
+        $this->assertEquals('Code success', $result);
+
+        // Verify exception
+        try {
+            $result = $handler->with(function () use (&$check) {
+                trigger_error('An error');
+                $check = true;
+                return 'Code success';
+            }, new RuntimeException('A provided exception', 23));
+        } catch (RuntimeException $e) {
+            $this->assertEquals('A provided exception', $e->getMessage());
+            $this->assertEquals(23, $e->getCode());
+            $this->assertNotNull($e->getPrevious());
+            $prev = $e->getPrevious();
+            $this->assertEquals('An error', $prev->getMessage());
+            $this->assertEquals(0, $prev->getCode());
+            $this->assertEquals(E_USER_NOTICE, $prev->getSeverity());
+            $this->assertNull($prev->getPrevious());
+        }
+        $this->assertFalse($check);
+
+         // Verify that exception is thrown
+        $this->expectException('RuntimeException');
+        $result = $handler->with(function () {
+            trigger_error('An error');
+            return 'Code success';
+        }, new RuntimeException('A provided exception', 23));
+    }
+
+    public function testWithCallback(): void
+    {
+        $handler = new ErrorHandler();
+        $check = false;
+
+        // No error invoked
+        $result = $handler->with(function () {
+            return 'Code success';
+        }, function ($error) {
+            return $error;
+        });
+        $this->assertEquals('Code success', $result);
+
+        // An error is invoked
+        $result = $handler->with(function () use (&$check) {
+            trigger_error('An error');
+            $check = true;
+            return 'Code success';
+        }, function ($error) {
+            return $error;
+        });
+        $this->assertFalse($check);
+
+        $this->assertEquals('An error', $result->getMessage());
+        $this->assertEquals(0, $result->getCode());
+        $this->assertEquals(E_USER_NOTICE, $result->getSeverity());
+        $this->assertNull($result->getPrevious());
+    }
+
+    public function testWithAllNull(): void
+    {
+        $handler = new ErrorHandler();
+        $check = false;
+
+        // No error invoked
+        $result = $handler->withAll(function () {
+            return 'Code success';
+        });
+        $this->assertEquals('Code success', $result);
+
+        // Verify exception
+        try {
+            $result = $handler->withAll(function () use (&$check) {
+                trigger_error('An error');
+                $check = true;
+                return 'Code success';
+            });
+        } catch (ErrorException $e) {
+            $this->assertEquals('An error', $e->getMessage());
+            $this->assertEquals(0, $e->getCode());
+            $this->assertEquals(E_USER_NOTICE, $e->getSeverity());
+            $this->assertNull($e->getPrevious());
+        }
+        $this->assertTrue($check);
+
+         // Verify that exception is thrown
+        $this->expectException('ErrorException');
+        $result = $handler->withAll(function () {
+            trigger_error('An error');
+            return 'Code success';
+        });
+    }
+
+    public function testWithAllThrowable(): void
+    {
+        $handler = new ErrorHandler();
+        $check = false;
+
+        // No exception
+        $result = $handler->withAll(function () {
+            return 'Code success';
+        });
+        $this->assertEquals('Code success', $result);
+
+        // Verify exception
+        try {
+            $result = $handler->withAll(function () use (&$check) {
+                trigger_error('An error');
+                $check = true;
+                return 'Code success';
+            }, new RuntimeException('A provided exception', 23));
+        } catch (RuntimeException $e) {
+            $this->assertEquals('A provided exception', $e->getMessage());
+            $this->assertEquals(23, $e->getCode());
+            $this->assertNotNull($e->getPrevious());
+            $prev = $e->getPrevious();
+            $this->assertEquals('An error', $prev->getMessage());
+            $this->assertEquals(0, $prev->getCode());
+            $this->assertEquals(E_USER_NOTICE, $prev->getSeverity());
+            $this->assertNull($prev->getPrevious());
+        }
+        $this->assertTrue($check);
+
+         // Verify that exception is thrown
+        $this->expectException('RuntimeException');
+        $result = $handler->withAll(function () {
+            trigger_error('An error');
+            return 'Code success';
+        }, new RuntimeException('A provided exception', 23));
+    }
+
+    public function testWithAllCallback(): void
+    {
+        $handler = new ErrorHandler();
+        $check = false;
+
+        // No error invoked
+        $result = $handler->withAll(function () {
+            return 'Code success';
+        }, function ($error, $result) {
+            return $error;
+        });
+        $this->assertEquals('Code success', $result);
+
+        // An error is invoked
+        $result = $handler->withAll(function () use (&$check) {
+            trigger_error('An error');
+            trigger_error('Another error', E_USER_WARNING);
+            $check = true;
+            return 'Code success';
+        }, function ($errors, $result) {
+            return ['errors' => $errors, 'result' => $result];
+        });
+        $this->assertTrue($check);
+
+        $this->assertEquals('Code success', $result['result']);
+        $this->assertEquals('An error', $result['errors'][0]->getMessage());
+        $this->assertEquals(0, $result['errors'][0]->getCode());
+        $this->assertEquals(E_USER_NOTICE, $result['errors'][0]->getSeverity());
+        $this->assertNull($result['errors'][0]->getPrevious());
+        $this->assertEquals('Another error', $result['errors'][1]->getMessage());
+        $this->assertEquals(0, $result['errors'][1]->getCode());
+        $this->assertEquals(E_USER_WARNING, $result['errors'][1]->getSeverity());
+        $this->assertNull($result['errors'][1]->getPrevious());
+    }
+}

+ 21 - 0
vendor/psr/http-factory/LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2018 PHP-FIG
+
+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.

+ 12 - 0
vendor/psr/http-factory/README.md

@@ -0,0 +1,12 @@
+HTTP Factories
+==============
+
+This repository holds all interfaces related to [PSR-17 (HTTP Factories)][psr-url].
+
+Note that this is not a HTTP Factory implementation of its own. It is merely interfaces that describe the components of a HTTP Factory.
+
+The installable [package][package-url] and [implementations][implementation-url] are listed on Packagist.
+
+[psr-url]: https://www.php-fig.org/psr/psr-17/
+[package-url]: https://packagist.org/packages/psr/http-factory
+[implementation-url]: https://packagist.org/providers/psr/http-factory-implementation

+ 35 - 0
vendor/psr/http-factory/composer.json

@@ -0,0 +1,35 @@
+{
+    "name": "psr/http-factory",
+    "description": "Common interfaces for PSR-7 HTTP message factories",
+    "keywords": [
+        "psr",
+        "psr-7",
+        "psr-17",
+        "http",
+        "factory",
+        "message",
+        "request",
+        "response"
+    ],
+    "license": "MIT",
+    "authors": [
+        {
+            "name": "PHP-FIG",
+            "homepage": "https://www.php-fig.org/"
+        }
+    ],
+    "require": {
+        "php": ">=7.0.0",
+        "psr/http-message": "^1.0 || ^2.0"
+    },
+    "autoload": {
+        "psr-4": {
+            "Psr\\Http\\Message\\": "src/"
+        }
+    },
+    "extra": {
+        "branch-alias": {
+            "dev-master": "1.0.x-dev"
+        }
+    }
+}

+ 18 - 0
vendor/psr/http-factory/src/RequestFactoryInterface.php

@@ -0,0 +1,18 @@
+<?php
+
+namespace Psr\Http\Message;
+
+interface RequestFactoryInterface
+{
+    /**
+     * Create a new request.
+     *
+     * @param string $method The HTTP method associated with the request.
+     * @param UriInterface|string $uri The URI associated with the request. If
+     *     the value is a string, the factory MUST create a UriInterface
+     *     instance based on it.
+     *
+     * @return RequestInterface
+     */
+    public function createRequest(string $method, $uri): RequestInterface;
+}

+ 18 - 0
vendor/psr/http-factory/src/ResponseFactoryInterface.php

@@ -0,0 +1,18 @@
+<?php
+
+namespace Psr\Http\Message;
+
+interface ResponseFactoryInterface
+{
+    /**
+     * Create a new response.
+     *
+     * @param int $code HTTP status code; defaults to 200
+     * @param string $reasonPhrase Reason phrase to associate with status code
+     *     in generated response; if none is provided implementations MAY use
+     *     the defaults as suggested in the HTTP specification.
+     *
+     * @return ResponseInterface
+     */
+    public function createResponse(int $code = 200, string $reasonPhrase = ''): ResponseInterface;
+}

+ 24 - 0
vendor/psr/http-factory/src/ServerRequestFactoryInterface.php

@@ -0,0 +1,24 @@
+<?php
+
+namespace Psr\Http\Message;
+
+interface ServerRequestFactoryInterface
+{
+    /**
+     * Create a new server request.
+     *
+     * Note that server-params are taken precisely as given - no parsing/processing
+     * of the given values is performed, and, in particular, no attempt is made to
+     * determine the HTTP method or URI, which must be provided explicitly.
+     *
+     * @param string $method The HTTP method associated with the request.
+     * @param UriInterface|string $uri The URI associated with the request. If
+     *     the value is a string, the factory MUST create a UriInterface
+     *     instance based on it.
+     * @param array $serverParams Array of SAPI parameters with which to seed
+     *     the generated request instance.
+     *
+     * @return ServerRequestInterface
+     */
+    public function createServerRequest(string $method, $uri, array $serverParams = []): ServerRequestInterface;
+}

+ 45 - 0
vendor/psr/http-factory/src/StreamFactoryInterface.php

@@ -0,0 +1,45 @@
+<?php
+
+namespace Psr\Http\Message;
+
+interface StreamFactoryInterface
+{
+    /**
+     * Create a new stream from a string.
+     *
+     * The stream SHOULD be created with a temporary resource.
+     *
+     * @param string $content String content with which to populate the stream.
+     *
+     * @return StreamInterface
+     */
+    public function createStream(string $content = ''): StreamInterface;
+
+    /**
+     * Create a stream from an existing file.
+     *
+     * The file MUST be opened using the given mode, which may be any mode
+     * supported by the `fopen` function.
+     *
+     * The `$filename` MAY be any string supported by `fopen()`.
+     *
+     * @param string $filename Filename or stream URI to use as basis of stream.
+     * @param string $mode Mode with which to open the underlying filename/stream.
+     *
+     * @return StreamInterface
+     * @throws \RuntimeException If the file cannot be opened.
+     * @throws \InvalidArgumentException If the mode is invalid.
+     */
+    public function createStreamFromFile(string $filename, string $mode = 'r'): StreamInterface;
+
+    /**
+     * Create a new stream from an existing resource.
+     *
+     * The stream MUST be readable and may be writable.
+     *
+     * @param resource $resource PHP resource to use as basis of stream.
+     *
+     * @return StreamInterface
+     */
+    public function createStreamFromResource($resource): StreamInterface;
+}

+ 34 - 0
vendor/psr/http-factory/src/UploadedFileFactoryInterface.php

@@ -0,0 +1,34 @@
+<?php
+
+namespace Psr\Http\Message;
+
+interface UploadedFileFactoryInterface
+{
+    /**
+     * Create a new uploaded file.
+     *
+     * If a size is not provided it will be determined by checking the size of
+     * the file.
+     *
+     * @see http://php.net/manual/features.file-upload.post-method.php
+     * @see http://php.net/manual/features.file-upload.errors.php
+     *
+     * @param StreamInterface $stream Underlying stream representing the
+     *     uploaded file content.
+     * @param int $size in bytes
+     * @param int $error PHP file upload error
+     * @param string $clientFilename Filename as provided by the client, if any.
+     * @param string $clientMediaType Media type as provided by the client, if any.
+     *
+     * @return UploadedFileInterface
+     *
+     * @throws \InvalidArgumentException If the file resource is not readable.
+     */
+    public function createUploadedFile(
+        StreamInterface $stream,
+        int $size = null,
+        int $error = \UPLOAD_ERR_OK,
+        string $clientFilename = null,
+        string $clientMediaType = null
+    ): UploadedFileInterface;
+}

+ 17 - 0
vendor/psr/http-factory/src/UriFactoryInterface.php

@@ -0,0 +1,17 @@
+<?php
+
+namespace Psr\Http\Message;
+
+interface UriFactoryInterface
+{
+    /**
+     * Create a new URI.
+     *
+     * @param string $uri
+     *
+     * @return UriInterface
+     *
+     * @throws \InvalidArgumentException If the given URI cannot be parsed.
+     */
+    public function createUri(string $uri = ''): UriInterface;
+}

+ 36 - 0
vendor/psr/http-message/CHANGELOG.md

@@ -0,0 +1,36 @@
+# Changelog
+
+All notable changes to this project will be documented in this file, in reverse chronological order by release.
+
+## 1.0.1 - 2016-08-06
+
+### Added
+
+- Nothing.
+
+### Deprecated
+
+- Nothing.
+
+### Removed
+
+- Nothing.
+
+### Fixed
+
+- Updated all `@return self` annotation references in interfaces to use
+  `@return static`, which more closelly follows the semantics of the
+  specification.
+- Updated the `MessageInterface::getHeaders()` return annotation to use the
+  value `string[][]`, indicating the format is a nested array of strings.
+- Updated the `@link` annotation for `RequestInterface::withRequestTarget()`
+  to point to the correct section of RFC 7230.
+- Updated the `ServerRequestInterface::withUploadedFiles()` parameter annotation
+  to add the parameter name (`$uploadedFiles`).
+- Updated a `@throws` annotation for the `UploadedFileInterface::moveTo()`
+  method to correctly reference the method parameter (it was referencing an
+  incorrect parameter name previously).
+
+## 1.0.0 - 2016-05-18
+
+Initial stable release; reflects accepted PSR-7 specification.

+ 19 - 0
vendor/psr/http-message/LICENSE

@@ -0,0 +1,19 @@
+Copyright (c) 2014 PHP Framework Interoperability Group
+
+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.

+ 16 - 0
vendor/psr/http-message/README.md

@@ -0,0 +1,16 @@
+PSR Http Message
+================
+
+This repository holds all interfaces/classes/traits related to
+[PSR-7](http://www.php-fig.org/psr/psr-7/).
+
+Note that this is not a HTTP message implementation of its own. It is merely an
+interface that describes a HTTP message. See the specification for more details.
+
+Usage
+-----
+
+Before reading the usage guide we recommend reading the PSR-7 interfaces method list:
+
+* [`PSR-7 Interfaces Method List`](docs/PSR7-Interfaces.md)
+* [`PSR-7 Usage Guide`](docs/PSR7-Usage.md)

+ 26 - 0
vendor/psr/http-message/composer.json

@@ -0,0 +1,26 @@
+{
+    "name": "psr/http-message",
+    "description": "Common interface for HTTP messages",
+    "keywords": ["psr", "psr-7", "http", "http-message", "request", "response"],
+    "homepage": "https://github.com/php-fig/http-message",
+    "license": "MIT",
+    "authors": [
+        {
+            "name": "PHP-FIG",
+            "homepage": "http://www.php-fig.org/"
+        }
+    ],
+    "require": {
+        "php": "^7.2 || ^8.0"
+    },
+    "autoload": {
+        "psr-4": {
+            "Psr\\Http\\Message\\": "src/"
+        }
+    },
+    "extra": {
+        "branch-alias": {
+            "dev-master": "1.1.x-dev"
+        }
+    }
+}

+ 130 - 0
vendor/psr/http-message/docs/PSR7-Interfaces.md

@@ -0,0 +1,130 @@
+# Interfaces
+
+The purpose of this list is to help in finding the methods when working with PSR-7. This can be considered as a cheatsheet for PSR-7 interfaces.
+
+The interfaces defined in PSR-7 are the following:
+
+| Class Name | Description |
+|---|---|
+| [Psr\Http\Message\MessageInterface](http://www.php-fig.org/psr/psr-7/#psrhttpmessagemessageinterface) | Representation of a HTTP message |
+| [Psr\Http\Message\RequestInterface](http://www.php-fig.org/psr/psr-7/#psrhttpmessagerequestinterface) | Representation of an outgoing, client-side request. |
+| [Psr\Http\Message\ServerRequestInterface](http://www.php-fig.org/psr/psr-7/#psrhttpmessageserverrequestinterface) | Representation of an incoming, server-side HTTP request. | 
+| [Psr\Http\Message\ResponseInterface](http://www.php-fig.org/psr/psr-7/#psrhttpmessageresponseinterface) | Representation of an outgoing, server-side response. |
+| [Psr\Http\Message\StreamInterface](http://www.php-fig.org/psr/psr-7/#psrhttpmessagestreaminterface) | Describes a data stream |
+| [Psr\Http\Message\UriInterface](http://www.php-fig.org/psr/psr-7/#psrhttpmessageuriinterface) | Value object representing a URI. |
+| [Psr\Http\Message\UploadedFileInterface](http://www.php-fig.org/psr/psr-7/#psrhttpmessageuploadedfileinterface) | Value object representing a file uploaded through an HTTP request. |
+
+## `Psr\Http\Message\MessageInterface` Methods
+
+| Method Name                        | Description | Notes |
+|------------------------------------| ----------- | ----- |
+| `getProtocolVersion()`             | Retrieve HTTP protocol version          |  1.0 or 1.1 |
+| `withProtocolVersion($version)`    | Returns new message instance with given HTTP protocol version          |      |
+| `getHeaders()`                     | Retrieve all HTTP Headers               | [Request Header List](https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Request_fields), [Response Header List](https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Response_fields)      |
+| `hasHeader($name)`                 | Checks if HTTP Header with given name exists  | |
+| `getHeader($name)`                 | Retrieves a array with the values for a single header | |
+| `getHeaderLine($name)`             | Retrieves a comma-separated string of the values for a single header |  |
+| `withHeader($name, $value)`        | Returns new message instance with given HTTP Header | if the header existed in the original instance, replaces the header value from the original message with the value provided when creating the new instance. |
+| `withAddedHeader($name, $value)`   | Returns new message instance with appended value to given header | If header already exists value will be appended, if not a new header will be created |
+| `withoutHeader($name)`             | Removes HTTP Header with given name| |
+| `getBody()`                        | Retrieves the HTTP Message Body | Returns object implementing `StreamInterface`|
+| `withBody(StreamInterface $body)`  | Returns new message instance with given HTTP Message Body | |
+
+
+## `Psr\Http\Message\RequestInterface` Methods
+
+Same methods as `Psr\Http\Message\MessageInterface`  + the following methods:
+
+| Method Name                        | Description | Notes |
+|------------------------------------| ----------- | ----- |
+| `getRequestTarget()`                | Retrieves the message's request target              | origin-form, absolute-form, authority-form, asterisk-form ([RFC7230](https://www.rfc-editor.org/rfc/rfc7230.txt)) |
+| `withRequestTarget($requestTarget)` | Return a new message instance with the specific request-target |      |
+| `getMethod()`                       | Retrieves the HTTP method of the request.  |  GET, HEAD, POST, PUT, DELETE, CONNECT, OPTIONS, TRACE (defined in [RFC7231](https://tools.ietf.org/html/rfc7231)), PATCH (defined in [RFC5789](https://tools.ietf.org/html/rfc5789)) |
+| `withMethod($method)`               | Returns a new message instance with the provided HTTP method  | |
+| `getUri()`                 | Retrieves the URI instance | |
+| `withUri(UriInterface $uri, $preserveHost = false)` | Returns a new message instance with the provided URI |  |
+
+
+## `Psr\Http\Message\ServerRequestInterface` Methods
+
+Same methods as `Psr\Http\Message\RequestInterface`  + the following methods:
+
+| Method Name                        | Description | Notes |
+|------------------------------------| ----------- | ----- |
+| `getServerParams() `               | Retrieve server parameters  | Typically derived from `$_SERVER`  |
+| `getCookieParams()`                | Retrieves cookies sent by the client to the server. | Typically derived from `$_COOKIES` |
+| `withCookieParams(array $cookies)` |  Returns a new request instance with the specified cookies      |   | 
+| `withQueryParams(array $query)` | Returns a new request instance with the specified query string arguments  |  |
+| `getUploadedFiles()` | Retrieve normalized file upload data  |  |
+| `withUploadedFiles(array $uploadedFiles)` | Returns a new request instance with the specified uploaded files  |  |
+| `getParsedBody()` | Retrieve any parameters provided in the request body  |  |
+| `withParsedBody($data)` | Returns a new request instance with the specified body parameters  |  |
+| `getAttributes()` | Retrieve attributes derived from the request  |  |
+| `getAttribute($name, $default = null)` | Retrieve a single derived request attribute  |  |
+| `withAttribute($name, $value)` | Returns a new request instance with the specified derived request attribute  |  |
+| `withoutAttribute($name)` | Returns a new request instance that without the specified derived request attribute  |  |
+
+## `Psr\Http\Message\ResponseInterface` Methods:
+
+Same methods as `Psr\Http\Message\MessageInterface`  + the following methods:
+
+| Method Name                        | Description | Notes |
+|------------------------------------| ----------- | ----- |
+| `getStatusCode()` | Gets the response status code. | |
+| `withStatus($code, $reasonPhrase = '')` | Returns a new response instance with the specified status code and, optionally, reason phrase. | |
+| `getReasonPhrase()` | Gets the response reason phrase associated with the status code. | |
+
+##  `Psr\Http\Message\StreamInterface` Methods
+
+| Method Name                        | Description | Notes |
+|------------------------------------| ----------- | ----- |
+| `__toString()` | Reads all data from the stream into a string, from the beginning to end. | |
+| `close()` | Closes the stream and any underlying resources. | |
+| `detach()` | Separates any underlying resources from the stream. | |
+| `getSize()` | Get the size of the stream if known. | |
+| `eof()` | Returns true if the stream is at the end of the stream.| |
+| `isSeekable()` |  Returns whether or not the stream is seekable. | |
+| `seek($offset, $whence = SEEK_SET)` | Seek to a position in the stream. | |
+| `rewind()` | Seek to the beginning of the stream. | |
+| `isWritable()` | Returns whether or not the stream is writable. | |
+| `write($string)` | Write data to the stream. | |
+| `isReadable()` | Returns whether or not the stream is readable. | |
+| `read($length)` | Read data from the stream. | |
+| `getContents()` | Returns the remaining contents in a string | |
+| `getMetadata($key = null)()` | Get stream metadata as an associative array or retrieve a specific key. | |
+
+## `Psr\Http\Message\UriInterface` Methods
+
+| Method Name                        | Description | Notes |
+|------------------------------------| ----------- | ----- |
+| `getScheme()` | Retrieve the scheme component of the URI. | |
+| `getAuthority()` | Retrieve the authority component of the URI. | |
+| `getUserInfo()` | Retrieve the user information component of the URI. | |
+| `getHost()` | Retrieve the host component of the URI. | |
+| `getPort()` | Retrieve the port component of the URI. | |
+| `getPath()` | Retrieve the path component of the URI. | |
+| `getQuery()` | Retrieve the query string of the URI. | |
+| `getFragment()` | Retrieve the fragment component of the URI. | |
+| `withScheme($scheme)` | Return an instance with the specified scheme. | |
+| `withUserInfo($user, $password = null)` | Return an instance with the specified user information. | |
+| `withHost($host)` | Return an instance with the specified host. | |
+| `withPort($port)` | Return an instance with the specified port. | |
+| `withPath($path)` | Return an instance with the specified path. | |
+| `withQuery($query)` | Return an instance with the specified query string. | |
+| `withFragment($fragment)` | Return an instance with the specified URI fragment. | |
+| `__toString()` | Return the string representation as a URI reference. | |
+
+## `Psr\Http\Message\UploadedFileInterface` Methods
+
+| Method Name                        | Description | Notes |
+|------------------------------------| ----------- | ----- |
+| `getStream()` | Retrieve a stream representing the uploaded file. | |
+| `moveTo($targetPath)` | Move the uploaded file to a new location. | |
+| `getSize()` | Retrieve the file size. | |
+| `getError()` | Retrieve the error associated with the uploaded file. | |
+| `getClientFilename()` | Retrieve the filename sent by the client. | |
+| `getClientMediaType()` | Retrieve the media type sent by the client. | |
+
+> `RequestInterface`, `ServerRequestInterface`, `ResponseInterface` extend `MessageInterface`  because the `Request` and the `Response` are `HTTP Messages`.
+> When using `ServerRequestInterface`, both `RequestInterface` and `Psr\Http\Message\MessageInterface` methods are considered.
+

+ 159 - 0
vendor/psr/http-message/docs/PSR7-Usage.md

@@ -0,0 +1,159 @@
+### PSR-7 Usage
+
+All PSR-7 applications comply with these interfaces 
+They were created to establish a standard between middleware implementations.
+
+> `RequestInterface`, `ServerRequestInterface`, `ResponseInterface` extend `MessageInterface`  because the `Request` and the `Response` are `HTTP Messages`.
+> When using `ServerRequestInterface`, both `RequestInterface` and `Psr\Http\Message\MessageInterface` methods are considered.
+
+
+The following examples will illustrate how basic operations are done in PSR-7.
+
+##### Examples
+
+
+For this examples to work (at least) a PSR-7 implementation package is required. (eg: zendframework/zend-diactoros, guzzlehttp/psr7, slim/slim, etc)
+All PSR-7 implementations should have the same behaviour.
+
+The following will be assumed: 
+`$request` is an object of `Psr\Http\Message\RequestInterface` and
+
+`$response` is an object implementing `Psr\Http\Message\RequestInterface`
+
+
+### Working with HTTP Headers
+
+#### Adding headers to response:
+
+```php
+$response->withHeader('My-Custom-Header', 'My Custom Message');
+```
+
+#### Appending values to headers
+
+```php
+$response->withAddedHeader('My-Custom-Header', 'The second message');
+```
+
+#### Checking if header exists:
+
+```php
+$request->hasHeader('My-Custom-Header'); // will return false
+$response->hasHeader('My-Custom-Header'); // will return true
+```
+
+> Note: My-Custom-Header was only added in the Response
+
+#### Getting comma-separated values from a header (also applies to request)
+
+```php
+// getting value from request headers
+$request->getHeaderLine('Content-Type'); // will return: "text/html; charset=UTF-8"
+// getting value from response headers
+$response->getHeaderLine('My-Custom-Header'); // will return:  "My Custom Message; The second message"
+```
+
+#### Getting array of value from a header (also applies to request)
+```php
+// getting value from request headers
+$request->getHeader('Content-Type'); // will return: ["text/html", "charset=UTF-8"]
+// getting value from response headers
+$response->getHeader('My-Custom-Header'); // will return:  ["My Custom Message",  "The second message"]
+```
+
+#### Removing headers from HTTP Messages
+```php
+// removing a header from Request, removing deprecated "Content-MD5" header
+$request->withoutHeader('Content-MD5'); 
+
+// removing a header from Response
+// effect: the browser won't know the size of the stream
+// the browser will download the stream till it ends
+$response->withoutHeader('Content-Length');
+```
+
+### Working with HTTP Message Body
+
+When working with the PSR-7 there are two methods of implementation:
+#### 1. Getting the body separately
+
+> This method makes the body handling easier to understand and is useful when repeatedly calling body methods. (You only call `getBody()` once). Using this method mistakes like `$response->write()` are also prevented.
+
+```php
+$body = $response->getBody();
+// operations on body, eg. read, write, seek
+// ...
+// replacing the old body
+$response->withBody($body); 
+// this last statement is optional as we working with objects
+// in this case the "new" body is same with the "old" one
+// the $body variable has the same value as the one in $request, only the reference is passed
+```
+
+#### 2. Working directly on response
+
+> This method is useful when only performing few operations as the `$request->getBody()` statement fragment is required
+
+```php
+$response->getBody()->write('hello');
+```
+
+### Getting the body contents
+
+The following snippet gets the contents of a stream contents.
+> Note: Streams must be rewinded, if content was written into streams, it will be ignored when calling `getContents()` because the stream pointer is set to the last character, which is `\0` - meaning end of stream.
+```php 
+$body = $response->getBody();
+$body->rewind(); // or $body->seek(0);
+$bodyText = $body->getContents();
+```
+> Note: If `$body->seek(1)` is called before `$body->getContents()`, the first character will be ommited as the starting pointer is set to `1`, not `0`. This is why using `$body->rewind()` is recommended.
+
+### Append to body
+
+```php
+$response->getBody()->write('Hello'); // writing directly
+$body = $request->getBody(); // which is a `StreamInterface`
+$body->write('xxxxx');
+```
+
+### Prepend to body
+Prepending is different when it comes to streams. The content must be copied before writing the content to be prepended.
+The following example will explain the behaviour of streams.
+
+```php
+// assuming our response is initially empty
+$body = $repsonse->getBody();
+// writing the string "abcd"
+$body->write('abcd');
+
+// seeking to start of stream
+$body->seek(0);
+// writing 'ef'
+$body->write('ef'); // at this point the stream contains "efcd"
+```
+
+#### Prepending by rewriting separately
+
+```php
+// assuming our response body stream only contains: "abcd"
+$body = $response->getBody();
+$body->rewind();
+$contents = $body->getContents(); // abcd
+// seeking the stream to beginning
+$body->rewind();
+$body->write('ef'); // stream contains "efcd"
+$body->write($contents); // stream contains "efabcd"
+```
+
+> Note: `getContents()` seeks the stream while reading it, therefore if the second `rewind()` method call was not present the stream would have resulted in `abcdefabcd` because the `write()` method appends to stream if not preceeded by `rewind()` or `seek(0)`.
+
+#### Prepending by using contents as a string
+```php
+$body = $response->getBody();
+$body->rewind();
+$contents = $body->getContents(); // efabcd
+$contents = 'ef'.$contents;
+$body->rewind();
+$body->write($contents);
+```

+ 189 - 0
vendor/psr/http-message/src/MessageInterface.php

@@ -0,0 +1,189 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Psr\Http\Message;
+
+/**
+ * HTTP messages consist of requests from a client to a server and responses
+ * from a server to a client. This interface defines the methods common to
+ * each.
+ *
+ * Messages are considered immutable; all methods that might change state MUST
+ * be implemented such that they retain the internal state of the current
+ * message and return an instance that contains the changed state.
+ *
+ * @link http://www.ietf.org/rfc/rfc7230.txt
+ * @link http://www.ietf.org/rfc/rfc7231.txt
+ */
+interface MessageInterface
+{
+    /**
+     * Retrieves the HTTP protocol version as a string.
+     *
+     * The string MUST contain only the HTTP version number (e.g., "1.1", "1.0").
+     *
+     * @return string HTTP protocol version.
+     */
+    public function getProtocolVersion();
+
+    /**
+     * Return an instance with the specified HTTP protocol version.
+     *
+     * The version string MUST contain only the HTTP version number (e.g.,
+     * "1.1", "1.0").
+     *
+     * This method MUST be implemented in such a way as to retain the
+     * immutability of the message, and MUST return an instance that has the
+     * new protocol version.
+     *
+     * @param string $version HTTP protocol version
+     * @return static
+     */
+    public function withProtocolVersion(string $version);
+
+    /**
+     * Retrieves all message header values.
+     *
+     * The keys represent the header name as it will be sent over the wire, and
+     * each value is an array of strings associated with the header.
+     *
+     *     // Represent the headers as a string
+     *     foreach ($message->getHeaders() as $name => $values) {
+     *         echo $name . ": " . implode(", ", $values);
+     *     }
+     *
+     *     // Emit headers iteratively:
+     *     foreach ($message->getHeaders() as $name => $values) {
+     *         foreach ($values as $value) {
+     *             header(sprintf('%s: %s', $name, $value), false);
+     *         }
+     *     }
+     *
+     * While header names are not case-sensitive, getHeaders() will preserve the
+     * exact case in which headers were originally specified.
+     *
+     * @return string[][] Returns an associative array of the message's headers. Each
+     *     key MUST be a header name, and each value MUST be an array of strings
+     *     for that header.
+     */
+    public function getHeaders();
+
+    /**
+     * Checks if a header exists by the given case-insensitive name.
+     *
+     * @param string $name Case-insensitive header field name.
+     * @return bool Returns true if any header names match the given header
+     *     name using a case-insensitive string comparison. Returns false if
+     *     no matching header name is found in the message.
+     */
+    public function hasHeader(string $name);
+
+    /**
+     * Retrieves a message header value by the given case-insensitive name.
+     *
+     * This method returns an array of all the header values of the given
+     * case-insensitive header name.
+     *
+     * If the header does not appear in the message, this method MUST return an
+     * empty array.
+     *
+     * @param string $name Case-insensitive header field name.
+     * @return string[] An array of string values as provided for the given
+     *    header. If the header does not appear in the message, this method MUST
+     *    return an empty array.
+     */
+    public function getHeader(string $name);
+
+    /**
+     * Retrieves a comma-separated string of the values for a single header.
+     *
+     * This method returns all of the header values of the given
+     * case-insensitive header name as a string concatenated together using
+     * a comma.
+     *
+     * NOTE: Not all header values may be appropriately represented using
+     * comma concatenation. For such headers, use getHeader() instead
+     * and supply your own delimiter when concatenating.
+     *
+     * If the header does not appear in the message, this method MUST return
+     * an empty string.
+     *
+     * @param string $name Case-insensitive header field name.
+     * @return string A string of values as provided for the given header
+     *    concatenated together using a comma. If the header does not appear in
+     *    the message, this method MUST return an empty string.
+     */
+    public function getHeaderLine(string $name);
+
+    /**
+     * Return an instance with the provided value replacing the specified header.
+     *
+     * While header names are case-insensitive, the casing of the header will
+     * be preserved by this function, and returned from getHeaders().
+     *
+     * This method MUST be implemented in such a way as to retain the
+     * immutability of the message, and MUST return an instance that has the
+     * new and/or updated header and value.
+     *
+     * @param string $name Case-insensitive header field name.
+     * @param string|string[] $value Header value(s).
+     * @return static
+     * @throws \InvalidArgumentException for invalid header names or values.
+     */
+    public function withHeader(string $name, $value);
+
+    /**
+     * Return an instance with the specified header appended with the given value.
+     *
+     * Existing values for the specified header will be maintained. The new
+     * value(s) will be appended to the existing list. If the header did not
+     * exist previously, it will be added.
+     *
+     * This method MUST be implemented in such a way as to retain the
+     * immutability of the message, and MUST return an instance that has the
+     * new header and/or value.
+     *
+     * @param string $name Case-insensitive header field name to add.
+     * @param string|string[] $value Header value(s).
+     * @return static
+     * @throws \InvalidArgumentException for invalid header names or values.
+     */
+    public function withAddedHeader(string $name, $value);
+
+    /**
+     * Return an instance without the specified header.
+     *
+     * Header resolution MUST be done without case-sensitivity.
+     *
+     * This method MUST be implemented in such a way as to retain the
+     * immutability of the message, and MUST return an instance that removes
+     * the named header.
+     *
+     * @param string $name Case-insensitive header field name to remove.
+     * @return static
+     */
+    public function withoutHeader(string $name);
+
+    /**
+     * Gets the body of the message.
+     *
+     * @return StreamInterface Returns the body as a stream.
+     */
+    public function getBody();
+
+    /**
+     * Return an instance with the specified message body.
+     *
+     * The body MUST be a StreamInterface object.
+     *
+     * This method MUST be implemented in such a way as to retain the
+     * immutability of the message, and MUST return a new instance that has the
+     * new body stream.
+     *
+     * @param StreamInterface $body Body.
+     * @return static
+     * @throws \InvalidArgumentException When the body is not valid.
+     */
+    public function withBody(StreamInterface $body);
+}

+ 131 - 0
vendor/psr/http-message/src/RequestInterface.php

@@ -0,0 +1,131 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Psr\Http\Message;
+
+/**
+ * Representation of an outgoing, client-side request.
+ *
+ * Per the HTTP specification, this interface includes properties for
+ * each of the following:
+ *
+ * - Protocol version
+ * - HTTP method
+ * - URI
+ * - Headers
+ * - Message body
+ *
+ * During construction, implementations MUST attempt to set the Host header from
+ * a provided URI if no Host header is provided.
+ *
+ * Requests are considered immutable; all methods that might change state MUST
+ * be implemented such that they retain the internal state of the current
+ * message and return an instance that contains the changed state.
+ */
+interface RequestInterface extends MessageInterface
+{
+    /**
+     * Retrieves the message's request target.
+     *
+     * Retrieves the message's request-target either as it will appear (for
+     * clients), as it appeared at request (for servers), or as it was
+     * specified for the instance (see withRequestTarget()).
+     *
+     * In most cases, this will be the origin-form of the composed URI,
+     * unless a value was provided to the concrete implementation (see
+     * withRequestTarget() below).
+     *
+     * If no URI is available, and no request-target has been specifically
+     * provided, this method MUST return the string "/".
+     *
+     * @return string
+     */
+    public function getRequestTarget();
+
+    /**
+     * Return an instance with the specific request-target.
+     *
+     * If the request needs a non-origin-form request-target — e.g., for
+     * specifying an absolute-form, authority-form, or asterisk-form —
+     * this method may be used to create an instance with the specified
+     * request-target, verbatim.
+     *
+     * This method MUST be implemented in such a way as to retain the
+     * immutability of the message, and MUST return an instance that has the
+     * changed request target.
+     *
+     * @link http://tools.ietf.org/html/rfc7230#section-5.3 (for the various
+     *     request-target forms allowed in request messages)
+     * @param string $requestTarget
+     * @return static
+     */
+    public function withRequestTarget(string $requestTarget);
+
+    /**
+     * Retrieves the HTTP method of the request.
+     *
+     * @return string Returns the request method.
+     */
+    public function getMethod();
+
+    /**
+     * Return an instance with the provided HTTP method.
+     *
+     * While HTTP method names are typically all uppercase characters, HTTP
+     * method names are case-sensitive and thus implementations SHOULD NOT
+     * modify the given string.
+     *
+     * This method MUST be implemented in such a way as to retain the
+     * immutability of the message, and MUST return an instance that has the
+     * changed request method.
+     *
+     * @param string $method Case-sensitive method.
+     * @return static
+     * @throws \InvalidArgumentException for invalid HTTP methods.
+     */
+    public function withMethod(string $method);
+
+    /**
+     * Retrieves the URI instance.
+     *
+     * This method MUST return a UriInterface instance.
+     *
+     * @link http://tools.ietf.org/html/rfc3986#section-4.3
+     * @return UriInterface Returns a UriInterface instance
+     *     representing the URI of the request.
+     */
+    public function getUri();
+
+    /**
+     * Returns an instance with the provided URI.
+     *
+     * This method MUST update the Host header of the returned request by
+     * default if the URI contains a host component. If the URI does not
+     * contain a host component, any pre-existing Host header MUST be carried
+     * over to the returned request.
+     *
+     * You can opt-in to preserving the original state of the Host header by
+     * setting `$preserveHost` to `true`. When `$preserveHost` is set to
+     * `true`, this method interacts with the Host header in the following ways:
+     *
+     * - If the Host header is missing or empty, and the new URI contains
+     *   a host component, this method MUST update the Host header in the returned
+     *   request.
+     * - If the Host header is missing or empty, and the new URI does not contain a
+     *   host component, this method MUST NOT update the Host header in the returned
+     *   request.
+     * - If a Host header is present and non-empty, this method MUST NOT update
+     *   the Host header in the returned request.
+     *
+     * This method MUST be implemented in such a way as to retain the
+     * immutability of the message, and MUST return an instance that has the
+     * new UriInterface instance.
+     *
+     * @link http://tools.ietf.org/html/rfc3986#section-4.3
+     * @param UriInterface $uri New request URI to use.
+     * @param bool $preserveHost Preserve the original state of the Host header.
+     * @return static
+     */
+    public function withUri(UriInterface $uri, bool $preserveHost = false);
+}

+ 70 - 0
vendor/psr/http-message/src/ResponseInterface.php

@@ -0,0 +1,70 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Psr\Http\Message;
+
+/**
+ * Representation of an outgoing, server-side response.
+ *
+ * Per the HTTP specification, this interface includes properties for
+ * each of the following:
+ *
+ * - Protocol version
+ * - Status code and reason phrase
+ * - Headers
+ * - Message body
+ *
+ * Responses are considered immutable; all methods that might change state MUST
+ * be implemented such that they retain the internal state of the current
+ * message and return an instance that contains the changed state.
+ */
+interface ResponseInterface extends MessageInterface
+{
+    /**
+     * Gets the response status code.
+     *
+     * The status code is a 3-digit integer result code of the server's attempt
+     * to understand and satisfy the request.
+     *
+     * @return int Status code.
+     */
+    public function getStatusCode();
+
+    /**
+     * Return an instance with the specified status code and, optionally, reason phrase.
+     *
+     * If no reason phrase is specified, implementations MAY choose to default
+     * to the RFC 7231 or IANA recommended reason phrase for the response's
+     * status code.
+     *
+     * This method MUST be implemented in such a way as to retain the
+     * immutability of the message, and MUST return an instance that has the
+     * updated status and reason phrase.
+     *
+     * @link http://tools.ietf.org/html/rfc7231#section-6
+     * @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
+     * @param int $code The 3-digit integer result code to set.
+     * @param string $reasonPhrase The reason phrase to use with the
+     *     provided status code; if none is provided, implementations MAY
+     *     use the defaults as suggested in the HTTP specification.
+     * @return static
+     * @throws \InvalidArgumentException For invalid status code arguments.
+     */
+    public function withStatus(int $code, string $reasonPhrase = '');
+
+    /**
+     * Gets the response reason phrase associated with the status code.
+     *
+     * Because a reason phrase is not a required element in a response
+     * status line, the reason phrase value MAY be null. Implementations MAY
+     * choose to return the default RFC 7231 recommended reason phrase (or those
+     * listed in the IANA HTTP Status Code Registry) for the response's
+     * status code.
+     *
+     * @link http://tools.ietf.org/html/rfc7231#section-6
+     * @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
+     * @return string Reason phrase; must return an empty string if none present.
+     */
+    public function getReasonPhrase();
+}

+ 263 - 0
vendor/psr/http-message/src/ServerRequestInterface.php

@@ -0,0 +1,263 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Psr\Http\Message;
+
+/**
+ * Representation of an incoming, server-side HTTP request.
+ *
+ * Per the HTTP specification, this interface includes properties for
+ * each of the following:
+ *
+ * - Protocol version
+ * - HTTP method
+ * - URI
+ * - Headers
+ * - Message body
+ *
+ * Additionally, it encapsulates all data as it has arrived to the
+ * application from the CGI and/or PHP environment, including:
+ *
+ * - The values represented in $_SERVER.
+ * - Any cookies provided (generally via $_COOKIE)
+ * - Query string arguments (generally via $_GET, or as parsed via parse_str())
+ * - Upload files, if any (as represented by $_FILES)
+ * - Deserialized body parameters (generally from $_POST)
+ *
+ * $_SERVER values MUST be treated as immutable, as they represent application
+ * state at the time of request; as such, no methods are provided to allow
+ * modification of those values. The other values provide such methods, as they
+ * can be restored from $_SERVER or the request body, and may need treatment
+ * during the application (e.g., body parameters may be deserialized based on
+ * content type).
+ *
+ * Additionally, this interface recognizes the utility of introspecting a
+ * request to derive and match additional parameters (e.g., via URI path
+ * matching, decrypting cookie values, deserializing non-form-encoded body
+ * content, matching authorization headers to users, etc). These parameters
+ * are stored in an "attributes" property.
+ *
+ * Requests are considered immutable; all methods that might change state MUST
+ * be implemented such that they retain the internal state of the current
+ * message and return an instance that contains the changed state.
+ */
+interface ServerRequestInterface extends RequestInterface
+{
+    /**
+     * Retrieve server parameters.
+     *
+     * Retrieves data related to the incoming request environment,
+     * typically derived from PHP's $_SERVER superglobal. The data IS NOT
+     * REQUIRED to originate from $_SERVER.
+     *
+     * @return array
+     */
+    public function getServerParams();
+
+    /**
+     * Retrieve cookies.
+     *
+     * Retrieves cookies sent by the client to the server.
+     *
+     * The data MUST be compatible with the structure of the $_COOKIE
+     * superglobal.
+     *
+     * @return array
+     */
+    public function getCookieParams();
+
+    /**
+     * Return an instance with the specified cookies.
+     *
+     * The data IS NOT REQUIRED to come from the $_COOKIE superglobal, but MUST
+     * be compatible with the structure of $_COOKIE. Typically, this data will
+     * be injected at instantiation.
+     *
+     * This method MUST NOT update the related Cookie header of the request
+     * instance, nor related values in the server params.
+     *
+     * This method MUST be implemented in such a way as to retain the
+     * immutability of the message, and MUST return an instance that has the
+     * updated cookie values.
+     *
+     * @param array $cookies Array of key/value pairs representing cookies.
+     * @return static
+     */
+    public function withCookieParams(array $cookies);
+
+    /**
+     * Retrieve query string arguments.
+     *
+     * Retrieves the deserialized query string arguments, if any.
+     *
+     * Note: the query params might not be in sync with the URI or server
+     * params. If you need to ensure you are only getting the original
+     * values, you may need to parse the query string from `getUri()->getQuery()`
+     * or from the `QUERY_STRING` server param.
+     *
+     * @return array
+     */
+    public function getQueryParams();
+
+    /**
+     * Return an instance with the specified query string arguments.
+     *
+     * These values SHOULD remain immutable over the course of the incoming
+     * request. They MAY be injected during instantiation, such as from PHP's
+     * $_GET superglobal, or MAY be derived from some other value such as the
+     * URI. In cases where the arguments are parsed from the URI, the data
+     * MUST be compatible with what PHP's parse_str() would return for
+     * purposes of how duplicate query parameters are handled, and how nested
+     * sets are handled.
+     *
+     * Setting query string arguments MUST NOT change the URI stored by the
+     * request, nor the values in the server params.
+     *
+     * This method MUST be implemented in such a way as to retain the
+     * immutability of the message, and MUST return an instance that has the
+     * updated query string arguments.
+     *
+     * @param array $query Array of query string arguments, typically from
+     *     $_GET.
+     * @return static
+     */
+    public function withQueryParams(array $query);
+
+    /**
+     * Retrieve normalized file upload data.
+     *
+     * This method returns upload metadata in a normalized tree, with each leaf
+     * an instance of Psr\Http\Message\UploadedFileInterface.
+     *
+     * These values MAY be prepared from $_FILES or the message body during
+     * instantiation, or MAY be injected via withUploadedFiles().
+     *
+     * @return array An array tree of UploadedFileInterface instances; an empty
+     *     array MUST be returned if no data is present.
+     */
+    public function getUploadedFiles();
+
+    /**
+     * Create a new instance with the specified uploaded files.
+     *
+     * This method MUST be implemented in such a way as to retain the
+     * immutability of the message, and MUST return an instance that has the
+     * updated body parameters.
+     *
+     * @param array $uploadedFiles An array tree of UploadedFileInterface instances.
+     * @return static
+     * @throws \InvalidArgumentException if an invalid structure is provided.
+     */
+    public function withUploadedFiles(array $uploadedFiles);
+
+    /**
+     * Retrieve any parameters provided in the request body.
+     *
+     * If the request Content-Type is either application/x-www-form-urlencoded
+     * or multipart/form-data, and the request method is POST, this method MUST
+     * return the contents of $_POST.
+     *
+     * Otherwise, this method may return any results of deserializing
+     * the request body content; as parsing returns structured content, the
+     * potential types MUST be arrays or objects only. A null value indicates
+     * the absence of body content.
+     *
+     * @return null|array|object The deserialized body parameters, if any.
+     *     These will typically be an array or object.
+     */
+    public function getParsedBody();
+
+    /**
+     * Return an instance with the specified body parameters.
+     *
+     * These MAY be injected during instantiation.
+     *
+     * If the request Content-Type is either application/x-www-form-urlencoded
+     * or multipart/form-data, and the request method is POST, use this method
+     * ONLY to inject the contents of $_POST.
+     *
+     * The data IS NOT REQUIRED to come from $_POST, but MUST be the results of
+     * deserializing the request body content. Deserialization/parsing returns
+     * structured data, and, as such, this method ONLY accepts arrays or objects,
+     * or a null value if nothing was available to parse.
+     *
+     * As an example, if content negotiation determines that the request data
+     * is a JSON payload, this method could be used to create a request
+     * instance with the deserialized parameters.
+     *
+     * This method MUST be implemented in such a way as to retain the
+     * immutability of the message, and MUST return an instance that has the
+     * updated body parameters.
+     *
+     * @param null|array|object $data The deserialized body data. This will
+     *     typically be in an array or object.
+     * @return static
+     * @throws \InvalidArgumentException if an unsupported argument type is
+     *     provided.
+     */
+    public function withParsedBody($data);
+
+    /**
+     * Retrieve attributes derived from the request.
+     *
+     * The request "attributes" may be used to allow injection of any
+     * parameters derived from the request: e.g., the results of path
+     * match operations; the results of decrypting cookies; the results of
+     * deserializing non-form-encoded message bodies; etc. Attributes
+     * will be application and request specific, and CAN be mutable.
+     *
+     * @return array Attributes derived from the request.
+     */
+    public function getAttributes();
+
+    /**
+     * Retrieve a single derived request attribute.
+     *
+     * Retrieves a single derived request attribute as described in
+     * getAttributes(). If the attribute has not been previously set, returns
+     * the default value as provided.
+     *
+     * This method obviates the need for a hasAttribute() method, as it allows
+     * specifying a default value to return if the attribute is not found.
+     *
+     * @see getAttributes()
+     * @param string $name The attribute name.
+     * @param mixed $default Default value to return if the attribute does not exist.
+     * @return mixed
+     */
+    public function getAttribute(string $name, $default = null);
+
+    /**
+     * Return an instance with the specified derived request attribute.
+     *
+     * This method allows setting a single derived request attribute as
+     * described in getAttributes().
+     *
+     * This method MUST be implemented in such a way as to retain the
+     * immutability of the message, and MUST return an instance that has the
+     * updated attribute.
+     *
+     * @see getAttributes()
+     * @param string $name The attribute name.
+     * @param mixed $value The value of the attribute.
+     * @return static
+     */
+    public function withAttribute(string $name, $value);
+
+    /**
+     * Return an instance that removes the specified derived request attribute.
+     *
+     * This method allows removing a single derived request attribute as
+     * described in getAttributes().
+     *
+     * This method MUST be implemented in such a way as to retain the
+     * immutability of the message, and MUST return an instance that removes
+     * the attribute.
+     *
+     * @see getAttributes()
+     * @param string $name The attribute name.
+     * @return static
+     */
+    public function withoutAttribute(string $name);
+}

+ 160 - 0
vendor/psr/http-message/src/StreamInterface.php

@@ -0,0 +1,160 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Psr\Http\Message;
+
+/**
+ * Describes a data stream.
+ *
+ * Typically, an instance will wrap a PHP stream; this interface provides
+ * a wrapper around the most common operations, including serialization of
+ * the entire stream to a string.
+ */
+interface StreamInterface
+{
+    /**
+     * Reads all data from the stream into a string, from the beginning to end.
+     *
+     * This method MUST attempt to seek to the beginning of the stream before
+     * reading data and read the stream until the end is reached.
+     *
+     * Warning: This could attempt to load a large amount of data into memory.
+     *
+     * This method MUST NOT raise an exception in order to conform with PHP's
+     * string casting operations.
+     *
+     * @see http://php.net/manual/en/language.oop5.magic.php#object.tostring
+     * @return string
+     */
+    public function __toString();
+
+    /**
+     * Closes the stream and any underlying resources.
+     *
+     * @return void
+     */
+    public function close();
+
+    /**
+     * Separates any underlying resources from the stream.
+     *
+     * After the stream has been detached, the stream is in an unusable state.
+     *
+     * @return resource|null Underlying PHP stream, if any
+     */
+    public function detach();
+
+    /**
+     * Get the size of the stream if known.
+     *
+     * @return int|null Returns the size in bytes if known, or null if unknown.
+     */
+    public function getSize();
+
+    /**
+     * Returns the current position of the file read/write pointer
+     *
+     * @return int Position of the file pointer
+     * @throws \RuntimeException on error.
+     */
+    public function tell();
+
+    /**
+     * Returns true if the stream is at the end of the stream.
+     *
+     * @return bool
+     */
+    public function eof();
+
+    /**
+     * Returns whether or not the stream is seekable.
+     *
+     * @return bool
+     */
+    public function isSeekable();
+
+    /**
+     * Seek to a position in the stream.
+     *
+     * @link http://www.php.net/manual/en/function.fseek.php
+     * @param int $offset Stream offset
+     * @param int $whence Specifies how the cursor position will be calculated
+     *     based on the seek offset. Valid values are identical to the built-in
+     *     PHP $whence values for `fseek()`.  SEEK_SET: Set position equal to
+     *     offset bytes SEEK_CUR: Set position to current location plus offset
+     *     SEEK_END: Set position to end-of-stream plus offset.
+     * @throws \RuntimeException on failure.
+     */
+    public function seek(int $offset, int $whence = SEEK_SET);
+
+    /**
+     * Seek to the beginning of the stream.
+     *
+     * If the stream is not seekable, this method will raise an exception;
+     * otherwise, it will perform a seek(0).
+     *
+     * @see seek()
+     * @link http://www.php.net/manual/en/function.fseek.php
+     * @throws \RuntimeException on failure.
+     */
+    public function rewind();
+
+    /**
+     * Returns whether or not the stream is writable.
+     *
+     * @return bool
+     */
+    public function isWritable();
+
+    /**
+     * Write data to the stream.
+     *
+     * @param string $string The string that is to be written.
+     * @return int Returns the number of bytes written to the stream.
+     * @throws \RuntimeException on failure.
+     */
+    public function write(string $string);
+
+    /**
+     * Returns whether or not the stream is readable.
+     *
+     * @return bool
+     */
+    public function isReadable();
+
+    /**
+     * Read data from the stream.
+     *
+     * @param int $length Read up to $length bytes from the object and return
+     *     them. Fewer than $length bytes may be returned if underlying stream
+     *     call returns fewer bytes.
+     * @return string Returns the data read from the stream, or an empty string
+     *     if no bytes are available.
+     * @throws \RuntimeException if an error occurs.
+     */
+    public function read(int $length);
+
+    /**
+     * Returns the remaining contents in a string
+     *
+     * @return string
+     * @throws \RuntimeException if unable to read or an error occurs while
+     *     reading.
+     */
+    public function getContents();
+
+    /**
+     * Get stream metadata as an associative array or retrieve a specific key.
+     *
+     * The keys returned are identical to the keys returned from PHP's
+     * stream_get_meta_data() function.
+     *
+     * @link http://php.net/manual/en/function.stream-get-meta-data.php
+     * @param string|null $key Specific metadata to retrieve.
+     * @return array|mixed|null Returns an associative array if no key is
+     *     provided. Returns a specific key value if a key is provided and the
+     *     value is found, or null if the key is not found.
+     */
+    public function getMetadata(?string $key = null);
+}

+ 125 - 0
vendor/psr/http-message/src/UploadedFileInterface.php

@@ -0,0 +1,125 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Psr\Http\Message;
+
+/**
+ * Value object representing a file uploaded through an HTTP request.
+ *
+ * Instances of this interface are considered immutable; all methods that
+ * might change state MUST be implemented such that they retain the internal
+ * state of the current instance and return an instance that contains the
+ * changed state.
+ */
+interface UploadedFileInterface
+{
+    /**
+     * Retrieve a stream representing the uploaded file.
+     *
+     * This method MUST return a StreamInterface instance, representing the
+     * uploaded file. The purpose of this method is to allow utilizing native PHP
+     * stream functionality to manipulate the file upload, such as
+     * stream_copy_to_stream() (though the result will need to be decorated in a
+     * native PHP stream wrapper to work with such functions).
+     *
+     * If the moveTo() method has been called previously, this method MUST raise
+     * an exception.
+     *
+     * @return StreamInterface Stream representation of the uploaded file.
+     * @throws \RuntimeException in cases when no stream is available or can be
+     *     created.
+     */
+    public function getStream();
+
+    /**
+     * Move the uploaded file to a new location.
+     *
+     * Use this method as an alternative to move_uploaded_file(). This method is
+     * guaranteed to work in both SAPI and non-SAPI environments.
+     * Implementations must determine which environment they are in, and use the
+     * appropriate method (move_uploaded_file(), rename(), or a stream
+     * operation) to perform the operation.
+     *
+     * $targetPath may be an absolute path, or a relative path. If it is a
+     * relative path, resolution should be the same as used by PHP's rename()
+     * function.
+     *
+     * The original file or stream MUST be removed on completion.
+     *
+     * If this method is called more than once, any subsequent calls MUST raise
+     * an exception.
+     *
+     * When used in an SAPI environment where $_FILES is populated, when writing
+     * files via moveTo(), is_uploaded_file() and move_uploaded_file() SHOULD be
+     * used to ensure permissions and upload status are verified correctly.
+     *
+     * If you wish to move to a stream, use getStream(), as SAPI operations
+     * cannot guarantee writing to stream destinations.
+     *
+     * @see http://php.net/is_uploaded_file
+     * @see http://php.net/move_uploaded_file
+     * @param string $targetPath Path to which to move the uploaded file.
+     * @throws \InvalidArgumentException if the $targetPath specified is invalid.
+     * @throws \RuntimeException on any error during the move operation, or on
+     *     the second or subsequent call to the method.
+     */
+    public function moveTo(string $targetPath);
+    
+    /**
+     * Retrieve the file size.
+     *
+     * Implementations SHOULD return the value stored in the "size" key of
+     * the file in the $_FILES array if available, as PHP calculates this based
+     * on the actual size transmitted.
+     *
+     * @return int|null The file size in bytes or null if unknown.
+     */
+    public function getSize();
+    
+    /**
+     * Retrieve the error associated with the uploaded file.
+     *
+     * The return value MUST be one of PHP's UPLOAD_ERR_XXX constants.
+     *
+     * If the file was uploaded successfully, this method MUST return
+     * UPLOAD_ERR_OK.
+     *
+     * Implementations SHOULD return the value stored in the "error" key of
+     * the file in the $_FILES array.
+     *
+     * @see http://php.net/manual/en/features.file-upload.errors.php
+     * @return int One of PHP's UPLOAD_ERR_XXX constants.
+     */
+    public function getError();
+    
+    /**
+     * Retrieve the filename sent by the client.
+     *
+     * Do not trust the value returned by this method. A client could send
+     * a malicious filename with the intention to corrupt or hack your
+     * application.
+     *
+     * Implementations SHOULD return the value stored in the "name" key of
+     * the file in the $_FILES array.
+     *
+     * @return string|null The filename sent by the client or null if none
+     *     was provided.
+     */
+    public function getClientFilename();
+    
+    /**
+     * Retrieve the media type sent by the client.
+     *
+     * Do not trust the value returned by this method. A client could send
+     * a malicious media type with the intention to corrupt or hack your
+     * application.
+     *
+     * Implementations SHOULD return the value stored in the "type" key of
+     * the file in the $_FILES array.
+     *
+     * @return string|null The media type sent by the client or null if none
+     *     was provided.
+     */
+    public function getClientMediaType();
+}

+ 326 - 0
vendor/psr/http-message/src/UriInterface.php

@@ -0,0 +1,326 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Psr\Http\Message;
+
+/**
+ * Value object representing a URI.
+ *
+ * This interface is meant to represent URIs according to RFC 3986 and to
+ * provide methods for most common operations. Additional functionality for
+ * working with URIs can be provided on top of the interface or externally.
+ * Its primary use is for HTTP requests, but may also be used in other
+ * contexts.
+ *
+ * Instances of this interface are considered immutable; all methods that
+ * might change state MUST be implemented such that they retain the internal
+ * state of the current instance and return an instance that contains the
+ * changed state.
+ *
+ * Typically the Host header will be also be present in the request message.
+ * For server-side requests, the scheme will typically be discoverable in the
+ * server parameters.
+ *
+ * @link http://tools.ietf.org/html/rfc3986 (the URI specification)
+ */
+interface UriInterface
+{
+    /**
+     * Retrieve the scheme component of the URI.
+     *
+     * If no scheme is present, this method MUST return an empty string.
+     *
+     * The value returned MUST be normalized to lowercase, per RFC 3986
+     * Section 3.1.
+     *
+     * The trailing ":" character is not part of the scheme and MUST NOT be
+     * added.
+     *
+     * @see https://tools.ietf.org/html/rfc3986#section-3.1
+     * @return string The URI scheme.
+     */
+    public function getScheme();
+
+    /**
+     * Retrieve the authority component of the URI.
+     *
+     * If no authority information is present, this method MUST return an empty
+     * string.
+     *
+     * The authority syntax of the URI is:
+     *
+     * <pre>
+     * [user-info@]host[:port]
+     * </pre>
+     *
+     * If the port component is not set or is the standard port for the current
+     * scheme, it SHOULD NOT be included.
+     *
+     * @see https://tools.ietf.org/html/rfc3986#section-3.2
+     * @return string The URI authority, in "[user-info@]host[:port]" format.
+     */
+    public function getAuthority();
+
+    /**
+     * Retrieve the user information component of the URI.
+     *
+     * If no user information is present, this method MUST return an empty
+     * string.
+     *
+     * If a user is present in the URI, this will return that value;
+     * additionally, if the password is also present, it will be appended to the
+     * user value, with a colon (":") separating the values.
+     *
+     * The trailing "@" character is not part of the user information and MUST
+     * NOT be added.
+     *
+     * @return string The URI user information, in "username[:password]" format.
+     */
+    public function getUserInfo();
+
+    /**
+     * Retrieve the host component of the URI.
+     *
+     * If no host is present, this method MUST return an empty string.
+     *
+     * The value returned MUST be normalized to lowercase, per RFC 3986
+     * Section 3.2.2.
+     *
+     * @see http://tools.ietf.org/html/rfc3986#section-3.2.2
+     * @return string The URI host.
+     */
+    public function getHost();
+
+    /**
+     * Retrieve the port component of the URI.
+     *
+     * If a port is present, and it is non-standard for the current scheme,
+     * this method MUST return it as an integer. If the port is the standard port
+     * used with the current scheme, this method SHOULD return null.
+     *
+     * If no port is present, and no scheme is present, this method MUST return
+     * a null value.
+     *
+     * If no port is present, but a scheme is present, this method MAY return
+     * the standard port for that scheme, but SHOULD return null.
+     *
+     * @return null|int The URI port.
+     */
+    public function getPort();
+
+    /**
+     * Retrieve the path component of the URI.
+     *
+     * The path can either be empty or absolute (starting with a slash) or
+     * rootless (not starting with a slash). Implementations MUST support all
+     * three syntaxes.
+     *
+     * Normally, the empty path "" and absolute path "/" are considered equal as
+     * defined in RFC 7230 Section 2.7.3. But this method MUST NOT automatically
+     * do this normalization because in contexts with a trimmed base path, e.g.
+     * the front controller, this difference becomes significant. It's the task
+     * of the user to handle both "" and "/".
+     *
+     * The value returned MUST be percent-encoded, but MUST NOT double-encode
+     * any characters. To determine what characters to encode, please refer to
+     * RFC 3986, Sections 2 and 3.3.
+     *
+     * As an example, if the value should include a slash ("/") not intended as
+     * delimiter between path segments, that value MUST be passed in encoded
+     * form (e.g., "%2F") to the instance.
+     *
+     * @see https://tools.ietf.org/html/rfc3986#section-2
+     * @see https://tools.ietf.org/html/rfc3986#section-3.3
+     * @return string The URI path.
+     */
+    public function getPath();
+
+    /**
+     * Retrieve the query string of the URI.
+     *
+     * If no query string is present, this method MUST return an empty string.
+     *
+     * The leading "?" character is not part of the query and MUST NOT be
+     * added.
+     *
+     * The value returned MUST be percent-encoded, but MUST NOT double-encode
+     * any characters. To determine what characters to encode, please refer to
+     * RFC 3986, Sections 2 and 3.4.
+     *
+     * As an example, if a value in a key/value pair of the query string should
+     * include an ampersand ("&") not intended as a delimiter between values,
+     * that value MUST be passed in encoded form (e.g., "%26") to the instance.
+     *
+     * @see https://tools.ietf.org/html/rfc3986#section-2
+     * @see https://tools.ietf.org/html/rfc3986#section-3.4
+     * @return string The URI query string.
+     */
+    public function getQuery();
+
+    /**
+     * Retrieve the fragment component of the URI.
+     *
+     * If no fragment is present, this method MUST return an empty string.
+     *
+     * The leading "#" character is not part of the fragment and MUST NOT be
+     * added.
+     *
+     * The value returned MUST be percent-encoded, but MUST NOT double-encode
+     * any characters. To determine what characters to encode, please refer to
+     * RFC 3986, Sections 2 and 3.5.
+     *
+     * @see https://tools.ietf.org/html/rfc3986#section-2
+     * @see https://tools.ietf.org/html/rfc3986#section-3.5
+     * @return string The URI fragment.
+     */
+    public function getFragment();
+
+    /**
+     * Return an instance with the specified scheme.
+     *
+     * This method MUST retain the state of the current instance, and return
+     * an instance that contains the specified scheme.
+     *
+     * Implementations MUST support the schemes "http" and "https" case
+     * insensitively, and MAY accommodate other schemes if required.
+     *
+     * An empty scheme is equivalent to removing the scheme.
+     *
+     * @param string $scheme The scheme to use with the new instance.
+     * @return static A new instance with the specified scheme.
+     * @throws \InvalidArgumentException for invalid or unsupported schemes.
+     */
+    public function withScheme(string $scheme);
+
+    /**
+     * Return an instance with the specified user information.
+     *
+     * This method MUST retain the state of the current instance, and return
+     * an instance that contains the specified user information.
+     *
+     * Password is optional, but the user information MUST include the
+     * user; an empty string for the user is equivalent to removing user
+     * information.
+     *
+     * @param string $user The user name to use for authority.
+     * @param null|string $password The password associated with $user.
+     * @return static A new instance with the specified user information.
+     */
+    public function withUserInfo(string $user, ?string $password = null);
+
+    /**
+     * Return an instance with the specified host.
+     *
+     * This method MUST retain the state of the current instance, and return
+     * an instance that contains the specified host.
+     *
+     * An empty host value is equivalent to removing the host.
+     *
+     * @param string $host The hostname to use with the new instance.
+     * @return static A new instance with the specified host.
+     * @throws \InvalidArgumentException for invalid hostnames.
+     */
+    public function withHost(string $host);
+
+    /**
+     * Return an instance with the specified port.
+     *
+     * This method MUST retain the state of the current instance, and return
+     * an instance that contains the specified port.
+     *
+     * Implementations MUST raise an exception for ports outside the
+     * established TCP and UDP port ranges.
+     *
+     * A null value provided for the port is equivalent to removing the port
+     * information.
+     *
+     * @param null|int $port The port to use with the new instance; a null value
+     *     removes the port information.
+     * @return static A new instance with the specified port.
+     * @throws \InvalidArgumentException for invalid ports.
+     */
+    public function withPort(?int $port);
+
+    /**
+     * Return an instance with the specified path.
+     *
+     * This method MUST retain the state of the current instance, and return
+     * an instance that contains the specified path.
+     *
+     * The path can either be empty or absolute (starting with a slash) or
+     * rootless (not starting with a slash). Implementations MUST support all
+     * three syntaxes.
+     *
+     * If the path is intended to be domain-relative rather than path relative then
+     * it must begin with a slash ("/"). Paths not starting with a slash ("/")
+     * are assumed to be relative to some base path known to the application or
+     * consumer.
+     *
+     * Users can provide both encoded and decoded path characters.
+     * Implementations ensure the correct encoding as outlined in getPath().
+     *
+     * @param string $path The path to use with the new instance.
+     * @return static A new instance with the specified path.
+     * @throws \InvalidArgumentException for invalid paths.
+     */
+    public function withPath(string $path);
+
+    /**
+     * Return an instance with the specified query string.
+     *
+     * This method MUST retain the state of the current instance, and return
+     * an instance that contains the specified query string.
+     *
+     * Users can provide both encoded and decoded query characters.
+     * Implementations ensure the correct encoding as outlined in getQuery().
+     *
+     * An empty query string value is equivalent to removing the query string.
+     *
+     * @param string $query The query string to use with the new instance.
+     * @return static A new instance with the specified query string.
+     * @throws \InvalidArgumentException for invalid query strings.
+     */
+    public function withQuery(string $query);
+
+    /**
+     * Return an instance with the specified URI fragment.
+     *
+     * This method MUST retain the state of the current instance, and return
+     * an instance that contains the specified URI fragment.
+     *
+     * Users can provide both encoded and decoded fragment characters.
+     * Implementations ensure the correct encoding as outlined in getFragment().
+     *
+     * An empty fragment value is equivalent to removing the fragment.
+     *
+     * @param string $fragment The fragment to use with the new instance.
+     * @return static A new instance with the specified fragment.
+     */
+    public function withFragment(string $fragment);
+
+    /**
+     * Return the string representation as a URI reference.
+     *
+     * Depending on which components of the URI are present, the resulting
+     * string is either a full URI or relative reference according to RFC 3986,
+     * Section 4.1. The method concatenates the various components of the URI,
+     * using the appropriate delimiters:
+     *
+     * - If a scheme is present, it MUST be suffixed by ":".
+     * - If an authority is present, it MUST be prefixed by "//".
+     * - The path can be concatenated without delimiters. But there are two
+     *   cases where the path has to be adjusted to make the URI reference
+     *   valid as PHP does not allow to throw an exception in __toString():
+     *     - If the path is rootless and an authority is present, the path MUST
+     *       be prefixed by "/".
+     *     - If the path is starting with more than one "/" and no authority is
+     *       present, the starting slashes MUST be reduced to one.
+     * - If a query is present, it MUST be prefixed by "?".
+     * - If a fragment is present, it MUST be prefixed by "#".
+     *
+     * @see http://tools.ietf.org/html/rfc3986#section-4.1
+     * @return string
+     */
+    public function __toString();
+}

+ 19 - 0
vendor/psr/log/LICENSE

@@ -0,0 +1,19 @@
+Copyright (c) 2012 PHP Framework Interoperability Group
+
+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.

+ 58 - 0
vendor/psr/log/README.md

@@ -0,0 +1,58 @@
+PSR Log
+=======
+
+This repository holds all interfaces/classes/traits related to
+[PSR-3](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md).
+
+Note that this is not a logger of its own. It is merely an interface that
+describes a logger. See the specification for more details.
+
+Installation
+------------
+
+```bash
+composer require psr/log
+```
+
+Usage
+-----
+
+If you need a logger, you can use the interface like this:
+
+```php
+<?php
+
+use Psr\Log\LoggerInterface;
+
+class Foo
+{
+    private $logger;
+
+    public function __construct(LoggerInterface $logger = null)
+    {
+        $this->logger = $logger;
+    }
+
+    public function doSomething()
+    {
+        if ($this->logger) {
+            $this->logger->info('Doing work');
+        }
+           
+        try {
+            $this->doSomethingElse();
+        } catch (Exception $exception) {
+            $this->logger->error('Oh no!', array('exception' => $exception));
+        }
+
+        // do something useful
+    }
+}
+```
+
+You can then pick one of the implementations of the interface to get a logger.
+
+If you want to implement the interface, you can require this package and
+implement `Psr\Log\LoggerInterface` in your code. Please read the
+[specification text](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md)
+for details.

+ 26 - 0
vendor/psr/log/composer.json

@@ -0,0 +1,26 @@
+{
+    "name": "psr/log",
+    "description": "Common interface for logging libraries",
+    "keywords": ["psr", "psr-3", "log"],
+    "homepage": "https://github.com/php-fig/log",
+    "license": "MIT",
+    "authors": [
+        {
+            "name": "PHP-FIG",
+            "homepage": "https://www.php-fig.org/"
+        }
+    ],
+    "require": {
+        "php": ">=8.0.0"
+    },
+    "autoload": {
+        "psr-4": {
+            "Psr\\Log\\": "src"
+        }
+    },
+    "extra": {
+        "branch-alias": {
+            "dev-master": "3.x-dev"
+        }
+    }
+}

+ 15 - 0
vendor/psr/log/src/AbstractLogger.php

@@ -0,0 +1,15 @@
+<?php
+
+namespace Psr\Log;
+
+/**
+ * This is a simple Logger implementation that other Loggers can inherit from.
+ *
+ * It simply delegates all log-level-specific methods to the `log` method to
+ * reduce boilerplate code that a simple Logger that does the same thing with
+ * messages regardless of the error level has to implement.
+ */
+abstract class AbstractLogger implements LoggerInterface
+{
+    use LoggerTrait;
+}

+ 7 - 0
vendor/psr/log/src/InvalidArgumentException.php

@@ -0,0 +1,7 @@
+<?php
+
+namespace Psr\Log;
+
+class InvalidArgumentException extends \InvalidArgumentException
+{
+}

+ 18 - 0
vendor/psr/log/src/LogLevel.php

@@ -0,0 +1,18 @@
+<?php
+
+namespace Psr\Log;
+
+/**
+ * Describes log levels.
+ */
+class LogLevel
+{
+    const EMERGENCY = 'emergency';
+    const ALERT     = 'alert';
+    const CRITICAL  = 'critical';
+    const ERROR     = 'error';
+    const WARNING   = 'warning';
+    const NOTICE    = 'notice';
+    const INFO      = 'info';
+    const DEBUG     = 'debug';
+}

+ 14 - 0
vendor/psr/log/src/LoggerAwareInterface.php

@@ -0,0 +1,14 @@
+<?php
+
+namespace Psr\Log;
+
+/**
+ * Describes a logger-aware instance.
+ */
+interface LoggerAwareInterface
+{
+    /**
+     * Sets a logger instance on the object.
+     */
+    public function setLogger(LoggerInterface $logger): void;
+}

+ 22 - 0
vendor/psr/log/src/LoggerAwareTrait.php

@@ -0,0 +1,22 @@
+<?php
+
+namespace Psr\Log;
+
+/**
+ * Basic Implementation of LoggerAwareInterface.
+ */
+trait LoggerAwareTrait
+{
+    /**
+     * The logger instance.
+     */
+    protected ?LoggerInterface $logger = null;
+
+    /**
+     * Sets a logger.
+     */
+    public function setLogger(LoggerInterface $logger): void
+    {
+        $this->logger = $logger;
+    }
+}

+ 97 - 0
vendor/psr/log/src/LoggerInterface.php

@@ -0,0 +1,97 @@
+<?php
+
+namespace Psr\Log;
+
+/**
+ * Describes a logger instance.
+ *
+ * The message MUST be a string or object implementing __toString().
+ *
+ * The message MAY contain placeholders in the form: {foo} where foo
+ * will be replaced by the context data in key "foo".
+ *
+ * The context array can contain arbitrary data. The only assumption that
+ * can be made by implementors is that if an Exception instance is given
+ * to produce a stack trace, it MUST be in a key named "exception".
+ *
+ * See https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md
+ * for the full interface specification.
+ */
+interface LoggerInterface
+{
+    /**
+     * System is unusable.
+     *
+     * @param mixed[] $context
+     */
+    public function emergency(string|\Stringable $message, array $context = []): void;
+
+    /**
+     * Action must be taken immediately.
+     *
+     * Example: Entire website down, database unavailable, etc. This should
+     * trigger the SMS alerts and wake you up.
+     *
+     * @param mixed[] $context
+     */
+    public function alert(string|\Stringable $message, array $context = []): void;
+
+    /**
+     * Critical conditions.
+     *
+     * Example: Application component unavailable, unexpected exception.
+     *
+     * @param mixed[] $context
+     */
+    public function critical(string|\Stringable $message, array $context = []): void;
+
+    /**
+     * Runtime errors that do not require immediate action but should typically
+     * be logged and monitored.
+     *
+     * @param mixed[] $context
+     */
+    public function error(string|\Stringable $message, array $context = []): void;
+
+    /**
+     * Exceptional occurrences that are not errors.
+     *
+     * Example: Use of deprecated APIs, poor use of an API, undesirable things
+     * that are not necessarily wrong.
+     *
+     * @param mixed[] $context
+     */
+    public function warning(string|\Stringable $message, array $context = []): void;
+
+    /**
+     * Normal but significant events.
+     *
+     * @param mixed[] $context
+     */
+    public function notice(string|\Stringable $message, array $context = []): void;
+
+    /**
+     * Interesting events.
+     *
+     * Example: User logs in, SQL logs.
+     *
+     * @param mixed[] $context
+     */
+    public function info(string|\Stringable $message, array $context = []): void;
+
+    /**
+     * Detailed debug information.
+     *
+     * @param mixed[] $context
+     */
+    public function debug(string|\Stringable $message, array $context = []): void;
+
+    /**
+     * Logs with an arbitrary level.
+     *
+     * @param mixed[] $context
+     *
+     * @throws \Psr\Log\InvalidArgumentException
+     */
+    public function log($level, string|\Stringable $message, array $context = []): void;
+}

+ 98 - 0
vendor/psr/log/src/LoggerTrait.php

@@ -0,0 +1,98 @@
+<?php
+
+namespace Psr\Log;
+
+/**
+ * This is a simple Logger trait that classes unable to extend AbstractLogger
+ * (because they extend another class, etc) can include.
+ *
+ * It simply delegates all log-level-specific methods to the `log` method to
+ * reduce boilerplate code that a simple Logger that does the same thing with
+ * messages regardless of the error level has to implement.
+ */
+trait LoggerTrait
+{
+    /**
+     * System is unusable.
+     */
+    public function emergency(string|\Stringable $message, array $context = []): void
+    {
+        $this->log(LogLevel::EMERGENCY, $message, $context);
+    }
+
+    /**
+     * Action must be taken immediately.
+     *
+     * Example: Entire website down, database unavailable, etc. This should
+     * trigger the SMS alerts and wake you up.
+     */
+    public function alert(string|\Stringable $message, array $context = []): void
+    {
+        $this->log(LogLevel::ALERT, $message, $context);
+    }
+
+    /**
+     * Critical conditions.
+     *
+     * Example: Application component unavailable, unexpected exception.
+     */
+    public function critical(string|\Stringable $message, array $context = []): void
+    {
+        $this->log(LogLevel::CRITICAL, $message, $context);
+    }
+
+    /**
+     * Runtime errors that do not require immediate action but should typically
+     * be logged and monitored.
+     */
+    public function error(string|\Stringable $message, array $context = []): void
+    {
+        $this->log(LogLevel::ERROR, $message, $context);
+    }
+
+    /**
+     * Exceptional occurrences that are not errors.
+     *
+     * Example: Use of deprecated APIs, poor use of an API, undesirable things
+     * that are not necessarily wrong.
+     */
+    public function warning(string|\Stringable $message, array $context = []): void
+    {
+        $this->log(LogLevel::WARNING, $message, $context);
+    }
+
+    /**
+     * Normal but significant events.
+     */
+    public function notice(string|\Stringable $message, array $context = []): void
+    {
+        $this->log(LogLevel::NOTICE, $message, $context);
+    }
+
+    /**
+     * Interesting events.
+     *
+     * Example: User logs in, SQL logs.
+     */
+    public function info(string|\Stringable $message, array $context = []): void
+    {
+        $this->log(LogLevel::INFO, $message, $context);
+    }
+
+    /**
+     * Detailed debug information.
+     */
+    public function debug(string|\Stringable $message, array $context = []): void
+    {
+        $this->log(LogLevel::DEBUG, $message, $context);
+    }
+
+    /**
+     * Logs with an arbitrary level.
+     *
+     * @param mixed $level
+     *
+     * @throws \Psr\Log\InvalidArgumentException
+     */
+    abstract public function log($level, string|\Stringable $message, array $context = []): void;
+}

+ 26 - 0
vendor/psr/log/src/NullLogger.php

@@ -0,0 +1,26 @@
+<?php
+
+namespace Psr\Log;
+
+/**
+ * This Logger can be used to avoid conditional log calls.
+ *
+ * Logging should always be optional, and if no logger is provided to your
+ * library creating a NullLogger instance to have something to throw logs at
+ * is a good way to avoid littering your code with `if ($this->logger) { }`
+ * blocks.
+ */
+class NullLogger extends AbstractLogger
+{
+    /**
+     * Logs with an arbitrary level.
+     *
+     * @param mixed[] $context
+     *
+     * @throws \Psr\Log\InvalidArgumentException
+     */
+    public function log($level, string|\Stringable $message, array $context = []): void
+    {
+        // noop
+    }
+}

+ 21 - 0
vendor/textalk/websocket/.github/ISSUE_TEMPLATE/bug_report.md

@@ -0,0 +1,21 @@
+---
+name: Bug report
+about: Use this if you believe there is a bug in this repo
+title: ''
+labels: bug
+assignees: ''
+
+---
+
+**Describe the bug**
+Please provide a clear and concise description of the suspected issue.
+
+**How to reproduce**
+If possible, provide information - possibly including code snippets - on how to reproduce the issue.
+
+**Logs**
+If possible, provide logs that indicate the issue. See https://github.com/Textalk/websocket-php/blob/master/docs/Examples.md#echo-logger on how to use the EchoLog. 
+
+**Versions**
+* Version of this library
+* PHP version

+ 14 - 0
vendor/textalk/websocket/.github/ISSUE_TEMPLATE/feature_request.md

@@ -0,0 +1,14 @@
+---
+name: Feature request
+about: Suggest an idea for this library
+title: ''
+labels: feature request
+assignees: ''
+
+---
+
+**Is it within the scope of this library?**
+Consider and describe why the feature would be beneficial in this library, and not implemented as a separate project using this as a dependency.
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.

+ 10 - 0
vendor/textalk/websocket/.github/ISSUE_TEMPLATE/other-issue.md

@@ -0,0 +1,10 @@
+---
+name: Other issue
+about: Use this for other issues
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Describe your issue**

+ 97 - 0
vendor/textalk/websocket/.github/workflows/acceptance.yml

@@ -0,0 +1,97 @@
+name: Acceptance
+
+on: [push, pull_request]
+
+jobs:
+  test-7-4:
+    runs-on: ubuntu-latest
+    name: Test PHP 7.4
+    steps:
+    - name: Checkout
+      uses: actions/checkout@v3
+    - name: Set up PHP 7.4
+      uses: shivammathur/setup-php@v2
+      with:
+        php-version: '7.4'
+    - name: Composer
+      run: make install
+    - name: Test
+      run: make test
+
+  test-8-0:
+    runs-on: ubuntu-latest
+    name: Test PHP 8.0
+    steps:
+    - name: Checkout
+      uses: actions/checkout@v3
+    - name: Set up PHP 8.0
+      uses: shivammathur/setup-php@v2
+      with:
+        php-version: '8.0'
+    - name: Composer
+      run: make install
+    - name: Test
+      run: make test
+
+  test-8-1:
+    runs-on: ubuntu-latest
+    name: Test PHP 8.1
+    steps:
+    - name: Checkout
+      uses: actions/checkout@v3
+    - name: Set up PHP 8.1
+      uses: shivammathur/setup-php@v2
+      with:
+        php-version: '8.1'
+    - name: Composer
+      run: make install
+    - name: Test
+      run: make test
+
+  test-8-2:
+    runs-on: ubuntu-latest
+    name: Test PHP 8.2
+    steps:
+    - name: Checkout
+      uses: actions/checkout@v3
+    - name: Set up PHP 8.2
+      uses: shivammathur/setup-php@v2
+      with:
+        php-version: '8.2'
+    - name: Composer
+      run: make install
+    - name: Test
+      run: make test
+
+  cs-check:
+    runs-on: ubuntu-latest
+    name: Code standard
+    steps:
+    - name: Checkout
+      uses: actions/checkout@v3
+    - name: Set up PHP 8.0
+      uses: shivammathur/setup-php@v2
+      with:
+        php-version: '8.0'
+    - name: Composer
+      run: make install
+    - name: Code standard
+      run: make cs-check
+
+  coverage:
+    runs-on: ubuntu-latest
+    name: Code coverage
+    steps:
+    - name: Checkout
+      uses: actions/checkout@v3
+    - name: Set up PHP 8.0
+      uses: shivammathur/setup-php@v2
+      with:
+        php-version: '8.0'
+        extensions: xdebug
+    - name: Composer
+      run: make install
+    - name: Code coverage
+      env:
+        COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+      run: make coverage

+ 6 - 0
vendor/textalk/websocket/.gitignore

@@ -0,0 +1,6 @@
+.DS_Store
+.phpunit.result.cache
+build/
+composer.lock
+composer.phar
+vendor/

+ 16 - 0
vendor/textalk/websocket/COPYING.md

@@ -0,0 +1,16 @@
+# Websocket: License
+
+Websocket PHP is free software released under the following license:
+
+[ISC License](http://en.wikipedia.org/wiki/ISC_license)
+
+Permission to use, copy, modify, and/or distribute this software for any purpose with or without
+fee is hereby granted, provided that the above copyright notice and this permission notice appear
+in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS
+SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
+AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
+THIS SOFTWARE.

+ 32 - 0
vendor/textalk/websocket/Makefile

@@ -0,0 +1,32 @@
+install: composer.phar
+	./composer.phar install
+
+update: composer.phar
+	./composer.phar self-update
+	./composer.phar update
+
+test: composer.lock
+	./vendor/bin/phpunit
+
+cs-check: composer.lock
+	./vendor/bin/phpcs --standard=PSR1,PSR12 --encoding=UTF-8 --report=full --colors lib tests examples
+
+coverage: composer.lock build
+	XDEBUG_MODE=coverage ./vendor/bin/phpunit --coverage-clover build/logs/clover.xml
+	./vendor/bin/php-coveralls -v
+
+composer.phar:
+	curl -s http://getcomposer.org/installer | php
+
+composer.lock: composer.phar
+	./composer.phar --no-interaction install
+
+vendor/bin/phpunit: install
+
+build:
+	mkdir build
+
+clean:
+	rm composer.phar
+	rm -r vendor
+	rm -r build

+ 67 - 0
vendor/textalk/websocket/README.md

@@ -0,0 +1,67 @@
+# Websocket Client and Server for PHP
+
+[![Build Status](https://github.com/Textalk/websocket-php/actions/workflows/acceptance.yml/badge.svg)](https://github.com/Textalk/websocket-php/actions)
+[![Coverage Status](https://coveralls.io/repos/github/Textalk/websocket-php/badge.svg?branch=master)](https://coveralls.io/github/Textalk/websocket-php)
+
+This library contains WebSocket client and server for PHP.
+
+The client and server provides methods for reading and writing to WebSocket streams.
+It does not include convenience operations such as listeners and implicit error handling.
+
+## Documentation
+
+- [Client](docs/Client.md)
+- [Server](docs/Server.md)
+- [Examples](docs/Examples.md)
+- [Changelog](docs/Changelog.md)
+- [Contributing](docs/Contributing.md)
+
+## Installing
+
+Preferred way to install is with [Composer](https://getcomposer.org/).
+```
+composer require textalk/websocket
+```
+
+* Current version support PHP versions `^7.4|^8.0`.
+* For PHP `7.2` and `7.3` support use version [`1.5`](https://github.com/Textalk/websocket-php/tree/1.5.0).
+* For PHP `7.1` support use version [`1.4`](https://github.com/Textalk/websocket-php/tree/1.4.0).
+* For PHP `^5.4` and `7.0` support use version [`1.3`](https://github.com/Textalk/websocket-php/tree/1.3.0).
+
+## Client
+
+The [client](docs/Client.md) can read and write on a WebSocket stream.
+It internally supports Upgrade handshake and implicit close and ping/pong operations.
+
+```php
+$client = new WebSocket\Client("ws://echo.websocket.org/");
+$client->text("Hello WebSocket.org!");
+echo $client->receive();
+$client->close();
+```
+
+## Server
+
+The library contains a rudimentary single stream/single thread [server](docs/Server.md).
+It internally supports Upgrade handshake and implicit close and ping/pong operations.
+
+Note that it does **not** support threading or automatic association ot continuous client requests.
+If you require this kind of server behavior, you need to build it on top of provided server implementation.
+
+```php
+$server = new WebSocket\Server();
+$server->accept();
+$message = $server->receive();
+$server->text($message);
+$server->close();
+```
+
+### License and Contributors
+
+[ISC License](COPYING.md)
+
+Fredrik Liljegren, Armen Baghumian Sankbarani, Ruslan Bekenev,
+Joshua Thijssen, Simon Lipp, Quentin Bellus, Patrick McCarren, swmcdonnell,
+Ignas Bernotas, Mark Herhold, Andreas Palm, Sören Jensen, pmaasz, Alexey Stavrov,
+Michael Slezak, Pierre Seznec, rmeisler, Nickolay V. Shmyrev, Christoph Kempen,
+Marc Roberts, Antonio Mora, Simon Podlipsky, etrinh.

+ 36 - 0
vendor/textalk/websocket/composer.json

@@ -0,0 +1,36 @@
+{
+    "name": "textalk/websocket",
+    "description": "WebSocket client and server",
+    "license": "ISC",
+    "type": "library",
+    "authors": [
+        {
+            "name": "Fredrik Liljegren"
+        },
+        {
+            "name": "Sören Jensen"
+        }
+    ],
+    "autoload": {
+        "psr-4": {
+            "WebSocket\\": "lib"
+        }
+    },
+    "autoload-dev": {
+        "psr-4": {
+            "WebSocket\\": "tests/mock"
+        }
+    },
+    "require": {
+        "php": "^7.4 | ^8.0",
+        "phrity/net-uri": "^1.0",
+        "phrity/util-errorhandler": "^1.0",
+        "psr/log": "^1.0 | ^2.0 | ^3.0",
+        "psr/http-message": "^1.0"
+    },
+    "require-dev": {
+        "phpunit/phpunit": "^9.0",
+        "php-coveralls/php-coveralls": "^2.0",
+        "squizlabs/php_codesniffer": "^3.5"
+    }
+}

+ 167 - 0
vendor/textalk/websocket/docs/Changelog.md

@@ -0,0 +1,167 @@
+[Client](Client.md) • [Server](Server.md) • [Message](Message.md) • [Examples](Examples.md) • Changelog • [Contributing](Contributing.md)
+
+# Websocket: Changelog
+
+## `v1.6`
+
+ > PHP version `^7.4|^8.0`
+
+### `1.6.3`
+
+ * Fix issue with implicit default ports (@etrinh, @sirn-se)
+
+### `1.6.2`
+
+ * Fix issue where port was missing in socket uri (@sirn-se)
+
+### `1.6.1`
+
+ * Fix client path for http request (@simPod, @sirn-se)
+
+### `1.6.0`
+ * Connection separate from Client and Server (@sirn-se)
+ * getPier() deprecated, replaced by getRemoteName() (@sirn-se)
+ * Client accepts `Psr\Http\Message\UriInterface` as input for URI:s (@sirn-se)
+ * Bad URI throws exception when Client is instanciated, previously when used (@sirn-se)
+ * Preparations for multiple conection and listeners (@sirn-se)
+ * Major internal refactoring (@sirn-se)
+
+## `v1.5`
+
+ > PHP version `^7.2|^8.0`
+
+### `1.5.8`
+
+ * Handle read error during handshake (@sirn-se)
+
+### `1.5.7`
+
+ * Large header block fix (@sirn-se)
+
+### `1.5.6`
+
+ * Add test for PHP 8.1 (@sirn-se)
+ * Code standard (@sirn-se)
+
+### `1.5.5`
+
+ * Support for psr/log v2 and v3 (@simPod)
+ * GitHub Actions replaces Travis (@sirn-se)
+
+### `1.5.4`
+
+ * Keep open connection on read timeout (@marcroberts)
+
+### `1.5.3`
+
+ * Fix for persistent connection (@sirn-se)
+
+### `1.5.2`
+
+ * Fix for getName() method (@sirn-se)
+
+### `1.5.1`
+
+ * Fix for persistent connections (@rmeisler)
+
+### `1.5.0`
+
+ * Convenience send methods; text(), binary(), ping(), pong() (@sirn-se)
+ * Optional Message instance as receive() method return (@sirn-se)
+ * Opcode filter for receive() method (@sirn-se)
+ * Added PHP `8.0` support (@webpatser)
+ * Dropped PHP `7.1` support (@sirn-se)
+ * Fix for unordered fragmented messages (@sirn-se)
+ * Improved error handling on stream calls (@sirn-se)
+ * Various code re-write (@sirn-se)
+
+## `v1.4`
+
+ > PHP version `^7.1`
+
+#### `1.4.3`
+
+ * Solve stream closure/get meta conflict (@sirn-se)
+ * Examples and documentation overhaul (@sirn-se)
+
+#### `1.4.2`
+
+ * Force stream close on read error (@sirn-se)
+ * Authorization headers line feed (@sirn-se)
+ * Documentation (@matias-pool, @sirn-se)
+
+#### `1.4.1`
+
+ * Ping/Pong, handled internally to avoid breaking fragmented messages (@nshmyrev, @sirn-se)
+ * Fix for persistent connections (@rmeisler)
+ * Fix opcode bitmask (@peterjah)
+
+#### `1.4.0`
+
+ * Dropped support of old PHP versions (@sirn-se)
+ * Added PSR-3 Logging support (@sirn-se)
+ * Persistent connection option (@slezakattack)
+ * TimeoutException on connection time out (@slezakattack)
+
+## `v1.3`
+
+ > PHP version `^5.4` and `^7.0`
+
+#### `1.3.1`
+
+ * Allow control messages without payload (@Logioniz)
+ * Error code in ConnectionException (@sirn-se)
+
+#### `1.3.0`
+
+ * Implements ping/pong frames (@pmccarren @Logioniz)
+ * Close behaviour (@sirn-se)
+ * Various fixes concerning connection handling (@sirn-se)
+ * Overhaul of Composer, Travis and Coveralls setup, PSR code standard and unit tests (@sirn-se)
+
+## `v1.2`
+
+ > PHP version `^5.4` and `^7.0`
+
+#### `1.2.0`
+
+ * Adding stream context options (to set e.g. SSL `allow_self_signed`).
+
+## `v1.1`
+
+ > PHP version `^5.4` and `^7.0`
+
+#### `1.1.2`
+
+ * Fixed error message on broken frame.
+
+#### `1.1.1`
+
+ * Adding license information.
+
+#### `1.1.0`
+
+ * Supporting huge payloads.
+
+## `v1.0`
+
+ > PHP version `^5.4` and `^7.0`
+
+#### `1.0.3`
+
+ * Bugfix: Correcting address in error-message
+
+#### `1.0.2`
+
+ * Bugfix: Add port in request-header.
+
+#### `1.0.1`
+
+ * Fixing a bug from empty payloads.
+
+#### `1.0.0`
+
+ * Release as production ready.
+ * Adding option to set/override headers.
+ * Supporting basic authentication from user:pass in URL.
+

+ 137 - 0
vendor/textalk/websocket/docs/Client.md

@@ -0,0 +1,137 @@
+Client • [Server](Server.md) • [Message](Message.md) • [Examples](Examples.md) • [Changelog](Changelog.md) • [Contributing](Contributing.md)
+
+# Websocket: Client
+
+The client can read and write on a WebSocket stream.
+It internally supports Upgrade handshake and implicit close and ping/pong operations.
+
+##  Class synopsis
+
+```php
+WebSocket\Client {
+
+    public __construct(UriInterface|string $uri, array $options = []);
+    public __destruct();
+    public __toString() : string;
+
+    public text(string $payload) : void;
+    public binary(string $payload) : void;
+    public ping(string $payload = '') : void;
+    public pong(string $payload = '') : void;
+    public send(Message|string $payload, string $opcode = 'text', bool $masked = true) : void;
+    public close(int $status = 1000, mixed $message = 'ttfn') : void;
+    public receive() : Message|string|null;
+
+    public getName() : string|null;
+    public getRemoteName() : string|null;
+    public getLastOpcode() : string;
+    public getCloseStatus() : int;
+    public isConnected() : bool;
+    public setTimeout(int $seconds) : void;
+    public setFragmentSize(int $fragment_size) : self;
+    public getFragmentSize() : int;
+    public setLogger(Psr\Log\LoggerInterface $logger = null) : void;
+}
+```
+
+## Examples
+
+### Simple send-receive operation
+
+This example send a single message to a server, and output the response.
+
+```php
+$client = new WebSocket\Client("ws://echo.websocket.org/");
+$client->text("Hello WebSocket.org!");
+echo $client->receive();
+$client->close();
+```
+
+### Listening to a server
+
+To continuously listen to incoming messages, you need to put the receive operation within a loop.
+Note that these functions **always** throw exception on any failure, including recoverable failures such as connection time out.
+By consuming exceptions, the code will re-connect the socket in next loop iteration.
+
+```php
+$client = new WebSocket\Client("ws://echo.websocket.org/");
+while (true) {
+    try {
+        $message = $client->receive();
+        // Act on received message
+        // Break while loop to stop listening
+    } catch (\WebSocket\ConnectionException $e) {
+        // Possibly log errors
+    }
+}
+$client->close();
+```
+
+### Filtering received messages
+
+By default the `receive()` method return messages of 'text' and 'binary' opcode.
+The filter option allows you to specify which message types to return.
+
+```php
+$client = new WebSocket\Client("ws://echo.websocket.org/", ['filter' => ['text']]);
+$client->receive(); // Only return 'text' messages
+
+$client = new WebSocket\Client("ws://echo.websocket.org/", ['filter' => ['text', 'binary', 'ping', 'pong', 'close']]);
+$client->receive(); // Return all messages
+```
+
+### Sending messages
+
+There are convenience methods to send messages with different opcodes.
+```php
+$client = new WebSocket\Client("ws://echo.websocket.org/");
+
+// Convenience methods
+$client->text('A plain text message'); // Send an opcode=text message
+$client->binary($binary_string); // Send an opcode=binary message
+$client->ping(); // Send an opcode=ping frame
+$client->pong(); // Send an unsolicited opcode=pong frame
+
+// Generic send method
+$client->send($payload); // Sent as masked opcode=text
+$client->send($payload, 'binary'); // Sent as masked opcode=binary
+$client->send($payload, 'binary', false); // Sent as unmasked opcode=binary
+```
+
+## Constructor options
+
+The `$options` parameter in constructor accepts an associative array of options.
+
+* `context` - A stream context created using [stream_context_create](https://www.php.net/manual/en/function.stream-context-create).
+* `filter` - Array of opcodes to return on receive, default `['text', 'binary']`
+* `fragment_size` - Maximum payload size. Default 4096 chars.
+* `headers` - Additional headers as associative array name => content.
+* `logger` - A [PSR-3](https://www.php-fig.org/psr/psr-3/) compatible logger.
+* `persistent` - Connection is re-used between requests until time out is reached. Default false.
+* `return_obj` - Return a [Message](Message.md) instance on receive, default false
+* `timeout` - Time out in seconds. Default 5 seconds.
+
+```php
+$context = stream_context_create();
+stream_context_set_option($context, 'ssl', 'verify_peer', false);
+stream_context_set_option($context, 'ssl', 'verify_peer_name', false);
+
+$client = new WebSocket\Client("ws://echo.websocket.org/", [
+    'context' => $context, // Attach stream context created above
+    'filter' => ['text', 'binary', 'ping'], // Specify message types for receive() to return
+    'headers' => [ // Additional headers, used to specify subprotocol
+        'Sec-WebSocket-Protocol' => 'soap',
+        'origin' => 'localhost',
+    ],
+    'logger' => $my_psr3_logger, // Attach a PSR3 compatible logger
+    'return_obj' => true, // Return Message instance rather than just text
+    'timeout' => 60, // 1 minute time out
+]);
+```
+
+## Exceptions
+
+* `WebSocket\BadOpcodeException` - Thrown if provided opcode is invalid.
+* `WebSocket\BadUriException` - Thrown if provided URI is invalid.
+* `WebSocket\ConnectionException` - Thrown on any socket I/O failure.
+* `WebSocket\TimeoutException` - Thrown when the socket experiences a time out.

+ 51 - 0
vendor/textalk/websocket/docs/Contributing.md

@@ -0,0 +1,51 @@
+[Client](Client.md) • [Server](Server.md) • [Message](Message.md) • [Examples](Examples.md) • [Changelog](Changelog.md) • Contributing
+
+# Websocket: Contributing
+
+Everyone is welcome to help out!
+But to keep this project sustainable, please ensure your contribution respects the requirements below.
+
+## PR Requirements
+
+Requirements on pull requests;
+* All tests **MUST** pass.
+* Code coverage **MUST** remain at 100%.
+* Code **MUST** adhere to PSR-1 and PSR-12 code standards.
+
+Base your patch on corresponding version branch, and target that version branch in your pull request.
+
+* `v1.6-master` current version
+* `v1.5-master` previous version, bug fixes only
+* Older versions should not be target of pull requests
+
+
+## Dependency management
+
+Install or update dependencies using [Composer](https://getcomposer.org/).
+
+```
+# Install dependencies
+make install
+
+# Update dependencies
+make update
+```
+
+## Code standard
+
+This project uses [PSR-1](https://www.php-fig.org/psr/psr-1/) and [PSR-12](https://www.php-fig.org/psr/psr-12/) code standards.
+```
+# Check code standard adherence
+make cs-check
+```
+
+## Unit testing
+
+Unit tests with [PHPUnit](https://phpunit.readthedocs.io/), coverage with [Coveralls](https://github.com/php-coveralls/php-coveralls)
+```
+# Run unit tests
+make test
+
+# Create coverage
+make coverage
+```

+ 101 - 0
vendor/textalk/websocket/docs/Examples.md

@@ -0,0 +1,101 @@
+[Client](Client.md) • [Server](Server.md) • [Message](Message.md) • Examples • [Changelog](Changelog.md) • [Contributing](Contributing.md)
+
+# Websocket: Examples
+
+Here are some examples on how to use the WebSocket library.
+
+##  Echo logger
+
+In dev environment (as in having run composer to include dev dependencies) you have
+access to a simple echo logger that print out information synchronously.
+
+This is usable for debugging. For production, use a proper logger.
+
+```php
+namespace WebSocket;
+
+$logger = new EchoLogger();
+
+$client = new Client('ws://echo.websocket.org/');
+$client->setLogger($logger);
+
+$server = new Server();
+$server->setLogger($logger);
+```
+
+An example of server output;
+```
+info     | Server listening to port 8000 []
+debug    | Wrote 129 of 129 bytes. []
+info     | Server connected to port 8000 []
+info     | Received 'text' message []
+debug    | Wrote 9 of 9 bytes. []
+info     | Sent 'text' message []
+debug    | Received 'close', status: 1000. []
+debug    | Wrote 32 of 32 bytes. []
+info     | Sent 'close' message []
+info     | Received 'close' message []
+```
+
+## The `send` client
+
+Source: [examples/send.php](../examples/send.php)
+
+A simple, single send/receive client.
+
+Example use:
+```
+php examples/send.php --opcode text "A text message" // Send a text message to localhost
+php examples/send.php --opcode ping "ping it" // Send a ping message to localhost
+php examples/send.php --uri ws://echo.websocket.org "A text message" // Send a text message to echo.websocket.org
+php examples/send.php --opcode text --debug "A text message" // Use runtime debugging
+```
+
+## The `echoserver` server
+
+Source: [examples/echoserver.php](../examples/echoserver.php)
+
+A simple server that responds to recevied commands.
+
+Example use:
+```
+php examples/echoserver.php // Run with default settings
+php examples/echoserver.php --port 8080 // Listen on port 8080
+php examples/echoserver.php --debug //  Use runtime debugging
+```
+
+These strings can be sent as message to trigger server to perform actions;
+* `auth` -  Server will respond with auth header if provided by client
+* `close` -  Server will close current connection
+* `exit` - Server will close all active connections
+* `headers` - Server will respond with all headers provided by client
+* `ping` - Server will send a ping message
+* `pong` - Server will send a pong message
+* `stop` - Server will stop listening
+* For other sent strings, server will respond with the same strings
+
+## The `random` client
+
+Source: [examples/random_client.php](../examples/random_client.php)
+
+The random client will use random options and continuously send/receive random messages.
+
+Example use:
+```
+php examples/random_client.php --uri ws://echo.websocket.org // Connect to echo.websocket.org
+php examples/random_client.php --timeout 5 --fragment_size 16 // Specify settings
+php examples/random_client.php --debug //  Use runtime debugging
+```
+
+## The `random` server
+
+Source: [examples/random_server.php](../examples/random_server.php)
+
+The random server will use random options and continuously send/receive random messages.
+
+Example use:
+```
+php examples/random_server.php --port 8080 // // Listen on port 8080
+php examples/random_server.php --timeout 5 --fragment_size 16 // Specify settings
+php examples/random_server.php --debug //  Use runtime debugging
+```

+ 60 - 0
vendor/textalk/websocket/docs/Message.md

@@ -0,0 +1,60 @@
+[Client](Client.md) • [Server](Server.md) • Message • [Examples](Examples.md) • [Changelog](Changelog.md) • [Contributing](Contributing.md)
+
+# Websocket: Messages
+
+If option `return_obj` is set to `true` on [client](Client.md) or [server](Server.md),
+the `receive()` method will return a Message instance instead of a string.
+
+Available classes correspond to opcode;
+* WebSocket\Message\Text
+* WebSocket\Message\Binary
+* WebSocket\Message\Ping
+* WebSocket\Message\Pong
+* WebSocket\Message\Close
+
+Additionally;
+* WebSocket\Message\Message - abstract base class for all messages above
+* WebSocket\Message\Factory - Factory class to create Message instances
+
+##  Message abstract class synopsis
+
+```php
+WebSocket\Message\Message {
+
+    public __construct(string $payload = '');
+    public __toString() : string;
+
+    public getOpcode() : string;
+    public getLength() : int;
+    public getTimestamp() : DateTime;
+    public getContent() : string;
+    public setContent(string $payload = '') : void;
+    public hasContent() : bool;
+}
+```
+
+##  Factory class synopsis
+
+```php
+WebSocket\Message\Factory {
+
+    public create(string $opcode, string $payload = '') : Message;
+}
+```
+
+## Example
+
+Receving a Message and echo some methods.
+
+```php
+$client = new WebSocket\Client('ws://echo.websocket.org/', ['return_obj' => true]);
+$client->text('Hello WebSocket.org!');
+// Echo return same message as sent
+$message = $client->receive();
+echo $message->getOpcode(); // -> "text"
+echo $message->getLength(); // -> 20
+echo $message->getContent(); // -> "Hello WebSocket.org!"
+echo $message->hasContent(); // -> true
+echo $message->getTimestamp()->format('H:i:s'); // -> 19:37:18
+$client->close();
+```

+ 136 - 0
vendor/textalk/websocket/docs/Server.md

@@ -0,0 +1,136 @@
+[Client](Client.md) • Server • [Message](Message.md) • [Examples](Examples.md) • [Changelog](Changelog.md) • [Contributing](Contributing.md)
+
+# Websocket: Server
+
+The library contains a rudimentary single stream/single thread server.
+It internally supports Upgrade handshake and implicit close and ping/pong operations.
+
+Note that it does **not** support threading or automatic association ot continuous client requests.
+If you require this kind of server behavior, you need to build it on top of provided server implementation.
+
+##  Class synopsis
+
+```php
+WebSocket\Server {
+
+    public __construct(array $options = []);
+    public __destruct();
+    public __toString() : string;
+
+    public accept() : bool;
+    public text(string $payload) : void;
+    public binary(string $payload) : void;
+    public ping(string $payload = '') : void;
+    public pong(string $payload = '') : void;
+    public send(Message|string $payload, string $opcode = 'text', bool $masked = true) : void;
+    public close(int $status = 1000, mixed $message = 'ttfn') : void;
+    public receive() : Message|string|null;
+
+    public getPort() : int;
+    public getPath() : string;
+    public getRequest() : array;
+    public getHeader(string $header_name) : string|null;
+
+    public getName() : string|null;
+    public getRemoteName() : string|null;
+    public getLastOpcode() : string;
+    public getCloseStatus() : int;
+    public isConnected() : bool;
+    public setTimeout(int $seconds) : void;
+    public setFragmentSize(int $fragment_size) : self;
+    public getFragmentSize() : int;
+    public setLogger(Psr\Log\LoggerInterface $logger = null) : void;
+}
+```
+
+## Examples
+
+### Simple receive-send operation
+
+This example reads a single message from a client, and respond with the same message.
+
+```php
+$server = new WebSocket\Server();
+$server->accept();
+$message = $server->receive();
+$server->text($message);
+$server->close();
+```
+
+### Listening to clients
+
+To continuously listen to incoming messages, you need to put the receive operation within a loop.
+Note that these functions **always** throw exception on any failure, including recoverable failures such as connection time out.
+By consuming exceptions, the code will re-connect the socket in next loop iteration.
+
+```php
+$server = new WebSocket\Server();
+while ($server->accept()) {
+    try {
+        $message = $server->receive();
+        // Act on received message
+        // Break while loop to stop listening
+    } catch (\WebSocket\ConnectionException $e) {
+        // Possibly log errors
+    }
+}
+$server->close();
+```
+
+### Filtering received messages
+
+By default the `receive()` method return messages of 'text' and 'binary' opcode.
+The filter option allows you to specify which message types to return.
+
+```php
+$server = new WebSocket\Server(['filter' => ['text']]);
+$server->receive(); // only return 'text' messages
+
+$server = new WebSocket\Server(['filter' => ['text', 'binary', 'ping', 'pong', 'close']]);
+$server->receive(); // return all messages
+```
+
+### Sending messages
+
+There are convenience methods to send messages with different opcodes.
+```php
+$server = new WebSocket\Server();
+
+// Convenience methods
+$server->text('A plain text message'); // Send an opcode=text message
+$server->binary($binary_string); // Send an opcode=binary message
+$server->ping(); // Send an opcode=ping frame
+$server->pong(); // Send an unsolicited opcode=pong frame
+
+// Generic send method
+$server->send($payload); // Sent as masked opcode=text
+$server->send($payload, 'binary'); // Sent as masked opcode=binary
+$server->send($payload, 'binary', false); // Sent as unmasked opcode=binary
+```
+
+## Constructor options
+
+The `$options` parameter in constructor accepts an associative array of options.
+
+* `filter` - Array of opcodes to return on receive, default `['text', 'binary']`
+* `fragment_size` - Maximum payload size. Default 4096 chars.
+* `logger` - A [PSR-3](https://www.php-fig.org/psr/psr-3/) compatible logger.
+* `port` - The server port to listen to. Default 8000.
+* `return_obj` - Return a [Message](Message.md) instance on receive, default false
+* `timeout` - Time out in seconds. Default 5 seconds.
+
+```php
+$server = new WebSocket\Server([
+    'filter' => ['text', 'binary', 'ping'], // Specify message types for receive() to return
+    'logger' => $my_psr3_logger, // Attach a PSR3 compatible logger
+    'port' => 9000, // Listening port
+    'return_obj' => true, // Return Message instance rather than just text
+    'timeout' => 60, // 1 minute time out
+]);
+```
+
+## Exceptions
+
+* `WebSocket\BadOpcodeException` - Thrown if provided opcode is invalid.
+* `WebSocket\ConnectionException` - Thrown on any socket I/O failure.
+* `WebSocket\TimeoutException` - Thrown when the socket experiences a time out.

+ 87 - 0
vendor/textalk/websocket/examples/echoserver.php

@@ -0,0 +1,87 @@
+<?php
+
+/**
+ * This file is used for the tests, but can also serve as an example of a WebSocket\Server.
+ * Run in console: php examples/echoserver.php
+ *
+ * Console options:
+ *  --port <int> : The port to listen to, default 8000
+ *  --timeout <int> : Timeout in seconds, default 200 seconds
+ *  --debug : Output log data (if logger is available)
+ */
+
+namespace WebSocket;
+
+require __DIR__ . '/../vendor/autoload.php';
+
+error_reporting(-1);
+
+echo "> Echo server\n";
+
+// Server options specified or random
+$options = array_merge([
+    'port'          => 8000,
+    'timeout'       => 200,
+    'filter'        => ['text', 'binary', 'ping', 'pong', 'close'],
+], getopt('', ['port:', 'timeout:', 'debug']));
+
+// If debug mode and logger is available
+if (isset($options['debug']) && class_exists('WebSocket\EchoLog')) {
+    $logger = new EchoLog();
+    $options['logger'] = $logger;
+    echo "> Using logger\n";
+}
+
+// Initiate server.
+try {
+    $server = new Server($options);
+} catch (ConnectionException $e) {
+    echo "> ERROR: {$e->getMessage()}\n";
+    die();
+}
+
+echo "> Listening to port {$server->getPort()}\n";
+
+// Force quit to close server
+while (true) {
+    try {
+        while ($server->accept()) {
+            echo "> Accepted on port {$server->getPort()}\n";
+            while (true) {
+                $message = $server->receive();
+                $opcode = $server->getLastOpcode();
+                if (is_null($message)) {
+                    echo "> Closing connection\n";
+                    continue 2;
+                }
+                echo "> Got '{$message}' [opcode: {$opcode}]\n";
+                if (in_array($opcode, ['ping', 'pong'])) {
+                    $server->send($message);
+                    continue;
+                }
+                // Allow certain string to trigger server action
+                switch ($message) {
+                    case 'exit':
+                        echo "> Client told me to quit.  Bye bye.\n";
+                        $server->close();
+                        echo "> Close status: {$server->getCloseStatus()}\n";
+                        exit;
+                    case 'headers':
+                        $server->text(implode("\r\n", $server->getRequest()));
+                        break;
+                    case 'ping':
+                        $server->ping($message);
+                        break;
+                    case 'auth':
+                        $auth = $server->getHeader('Authorization');
+                        $server->text("{$auth} - {$message}");
+                        break;
+                    default:
+                        $server->text($message);
+                }
+            }
+        }
+    } catch (ConnectionException $e) {
+        echo "> ERROR: {$e->getMessage()}\n";
+    }
+}

+ 94 - 0
vendor/textalk/websocket/examples/random_client.php

@@ -0,0 +1,94 @@
+<?php
+
+/**
+ * Websocket client that read/write random data.
+ * Run in console: php examples/random_client.php
+ *
+ * Console options:
+ *  --uri <uri> : The URI to connect to, default ws://localhost:8000
+ *  --timeout <int> : Timeout in seconds, random default
+ *  --fragment_size <int> : Fragment size as bytes, random default
+ *  --debug : Output log data (if logger is available)
+ */
+
+namespace WebSocket;
+
+require __DIR__ . '/../vendor/autoload.php';
+
+error_reporting(-1);
+
+$randStr = function (int $maxlength = 4096) {
+    $string = '';
+    $length = rand(1, $maxlength);
+    for ($i = 0; $i < $length; $i++) {
+        $string .= chr(rand(33, 126));
+    }
+    return $string;
+};
+
+echo "> Random client\n";
+
+// Server options specified or random
+$options = array_merge([
+    'uri'           => 'ws://localhost:8000',
+    'timeout'       => rand(1, 60),
+    'fragment_size' => rand(1, 4096) * 8,
+], getopt('', ['uri:', 'timeout:', 'fragment_size:', 'debug']));
+
+// If debug mode and logger is available
+if (isset($options['debug']) && class_exists('WebSocket\EchoLog')) {
+    $logger = new EchoLog();
+    $options['logger'] = $logger;
+    echo "> Using logger\n";
+}
+
+// Main loop
+while (true) {
+    try {
+        $client = new Client($options['uri'], $options);
+        $info = json_encode([
+            'uri'           => $options['uri'],
+            'timeout'       => $options['timeout'],
+            'framgemt_size' => $client->getFragmentSize(),
+        ]);
+        echo "> Creating client {$info}\n";
+
+        try {
+            while (true) {
+                // Random actions
+                switch (rand(1, 10)) {
+                    case 1:
+                        echo "> Sending text\n";
+                        $client->text("Text message {$randStr()}");
+                        break;
+                    case 2:
+                        echo "> Sending binary\n";
+                        $client->binary("Binary message {$randStr()}");
+                        break;
+                    case 3:
+                        echo "> Sending close\n";
+                        $client->close(rand(1000, 2000), "Close message {$randStr(8)}");
+                        break;
+                    case 4:
+                        echo "> Sending ping\n";
+                        $client->ping("Ping message  {$randStr(8)}");
+                        break;
+                    case 5:
+                        echo "> Sending pong\n";
+                        $client->pong("Pong message  {$randStr(8)}");
+                        break;
+                    default:
+                        echo "> Receiving\n";
+                        $received = $client->receive();
+                        echo "> Received {$client->getLastOpcode()}: {$received}\n";
+                }
+                sleep(rand(1, 5));
+            }
+        } catch (\Throwable $e) {
+            echo "ERROR I/O: {$e->getMessage()} [{$e->getCode()}]\n";
+        }
+    } catch (\Throwable $e) {
+        echo "ERROR: {$e->getMessage()} [{$e->getCode()}]\n";
+    }
+    sleep(rand(1, 5));
+}

+ 93 - 0
vendor/textalk/websocket/examples/random_server.php

@@ -0,0 +1,93 @@
+<?php
+
+/**
+ * Websocket server that read/write random data.
+ * Run in console: php examples/random_server.php
+ *
+ * Console options:
+ *  --port <int> : The port to listen to, default 8000
+ *  --timeout <int> : Timeout in seconds, random default
+ *  --fragment_size <int> : Fragment size as bytes, random default
+ *  --debug : Output log data (if logger is available)
+ */
+
+namespace WebSocket;
+
+require __DIR__ . '/../vendor/autoload.php';
+
+error_reporting(-1);
+
+$randStr = function (int $maxlength = 4096) {
+    $string = '';
+    $length = rand(1, $maxlength);
+    for ($i = 0; $i < $length; $i++) {
+        $string .= chr(rand(33, 126));
+    }
+    return $string;
+};
+
+echo "> Random server\n";
+
+// Server options specified or random
+$options = array_merge([
+    'port'          => 8000,
+    'timeout'       => rand(1, 60),
+    'fragment_size' => rand(1, 4096) * 8,
+], getopt('', ['port:', 'timeout:', 'fragment_size:', 'debug']));
+
+// If debug mode and logger is available
+if (isset($options['debug']) && class_exists('WebSocket\EchoLog')) {
+    $logger = new EchoLog();
+    $options['logger'] = $logger;
+    echo "> Using logger\n";
+}
+
+// Force quit to close server
+while (true) {
+    try {
+        // Setup server
+        $server = new Server($options);
+        $info = json_encode([
+          'port'          => $server->getPort(),
+          'timeout'       => $options['timeout'],
+          'framgemt_size' => $server->getFragmentSize(),
+        ]);
+        echo "> Creating server {$info}\n";
+
+        while ($server->accept()) {
+            while (true) {
+                // Random actions
+                switch (rand(1, 10)) {
+                    case 1:
+                        echo "> Sending text\n";
+                        $server->text("Text message {$randStr()}");
+                        break;
+                    case 2:
+                        echo "> Sending binary\n";
+                        $server->binary("Binary message {$randStr()}");
+                        break;
+                    case 3:
+                        echo "> Sending close\n";
+                        $server->close(rand(1000, 2000), "Close message {$randStr(8)}");
+                        break;
+                    case 4:
+                        echo "> Sending ping\n";
+                        $server->ping("Ping message {$randStr(8)}");
+                        break;
+                    case 5:
+                        echo "> Sending pong\n";
+                        $server->pong("Pong message {$randStr(8)}");
+                        break;
+                    default:
+                        echo "> Receiving\n";
+                        $received = $server->receive();
+                        echo "> Received {$server->getLastOpcode()}: {$received}\n";
+                }
+                sleep(rand(1, 5));
+            }
+        }
+    } catch (\Throwable $e) {
+        echo "ERROR: {$e->getMessage()} [{$e->getCode()}]\n";
+    }
+    sleep(rand(1, 5));
+}

+ 51 - 0
vendor/textalk/websocket/examples/send.php

@@ -0,0 +1,51 @@
+<?php
+
+/**
+ * Simple send & receive client for test purpose.
+ * Run in console: php examples/send.php <options> <message>
+ *
+ * Console options:
+ *  --uri <uri> : The URI to connect to, default ws://localhost:8000
+ *  --opcode <string> : Opcode to send, default 'text'
+ *  --debug : Output log data (if logger is available)
+ */
+
+namespace WebSocket;
+
+require __DIR__ . '/../vendor/autoload.php';
+
+error_reporting(-1);
+
+echo "> Send client\n";
+
+// Server options specified or random
+$options = array_merge([
+    'uri'           => 'ws://localhost:8000',
+    'opcode'        => 'text',
+], getopt('', ['uri:', 'opcode:', 'debug']));
+$message = array_pop($argv);
+
+// If debug mode and logger is available
+if (isset($options['debug']) && class_exists('WebSocket\EchoLog')) {
+    $logger = new EchoLog();
+    $options['logger'] = $logger;
+    echo "> Using logger\n";
+}
+
+try {
+    // Create client, send and recevie
+    $client = new Client($options['uri'], $options);
+    $client->send($message, $options['opcode']);
+    echo "> Sent '{$message}' [opcode: {$options['opcode']}]\n";
+    if (in_array($options['opcode'], ['text', 'binary'])) {
+        $message = $client->receive();
+        $opcode = $client->getLastOpcode();
+        if (!is_null($message)) {
+            echo "> Got '{$message}' [opcode: {$opcode}]\n";
+        }
+    }
+    $client->close();
+    echo "> Closing client\n";
+} catch (\Throwable $e) {
+    echo "ERROR: {$e->getMessage()} [{$e->getCode()}]\n";
+}

+ 14 - 0
vendor/textalk/websocket/lib/BadOpcodeException.php

@@ -0,0 +1,14 @@
+<?php
+
+/**
+ * Copyright (C) 2014-2022 Textalk/Abicart and contributors.
+ *
+ * This file is part of Websocket PHP and is free software under the ISC License.
+ * License text: https://raw.githubusercontent.com/Textalk/websocket-php/master/COPYING
+ */
+
+namespace WebSocket;
+
+class BadOpcodeException extends Exception
+{
+}

+ 7 - 0
vendor/textalk/websocket/lib/BadUriException.php

@@ -0,0 +1,7 @@
+<?php
+
+namespace WebSocket;
+
+class BadUriException extends Exception
+{
+}

+ 490 - 0
vendor/textalk/websocket/lib/Client.php

@@ -0,0 +1,490 @@
+<?php
+
+/**
+ * Copyright (C) 2014-2022 Textalk/Abicart and contributors.
+ *
+ * This file is part of Websocket PHP and is free software under the ISC License.
+ * License text: https://raw.githubusercontent.com/Textalk/websocket-php/master/COPYING
+ */
+
+namespace WebSocket;
+
+use ErrorException;
+use InvalidArgumentException;
+use Phrity\Net\Uri;
+use Phrity\Util\ErrorHandler;
+use Psr\Http\Message\UriInterface;
+use Psr\Log\{
+    LoggerAwareInterface,
+    LoggerAwareTrait,
+    LoggerInterface,
+    NullLogger
+};
+use WebSocket\Message\Factory;
+
+class Client implements LoggerAwareInterface
+{
+    use LoggerAwareTrait; // provides setLogger(LoggerInterface $logger)
+    use OpcodeTrait;
+
+    // Default options
+    protected static $default_options = [
+        'context'       => null,
+        'filter'        => ['text', 'binary'],
+        'fragment_size' => 4096,
+        'headers'       => null,
+        'logger'        => null,
+        'origin'        => null, // @deprecated
+        'persistent'    => false,
+        'return_obj'    => false,
+        'timeout'       => 5,
+    ];
+
+    private $socket_uri;
+    private $connection;
+    private $options = [];
+    private $listen = false;
+    private $last_opcode = null;
+
+
+    /* ---------- Magic methods ------------------------------------------------------ */
+
+    /**
+     * @param UriInterface|string $uri     A ws/wss-URI
+     * @param array               $options
+     *   Associative array containing:
+     *   - context:       Set the stream context. Default: empty context
+     *   - timeout:       Set the socket timeout in seconds.  Default: 5
+     *   - fragment_size: Set framgemnt size.  Default: 4096
+     *   - headers:       Associative array of headers to set/override.
+     */
+    public function __construct($uri, array $options = [])
+    {
+        $this->socket_uri = $this->parseUri($uri);
+        $this->options = array_merge(self::$default_options, [
+            'logger' => new NullLogger(),
+        ], $options);
+        $this->setLogger($this->options['logger']);
+    }
+
+    /**
+     * Get string representation of instance.
+     * @return string String representation.
+     */
+    public function __toString(): string
+    {
+        return sprintf(
+            "%s(%s)",
+            get_class($this),
+            $this->getName() ?: 'closed'
+        );
+    }
+
+
+    /* ---------- Client option functions -------------------------------------------- */
+
+    /**
+     * Set timeout.
+     * @param int $timeout Timeout in seconds.
+     */
+    public function setTimeout(int $timeout): void
+    {
+        $this->options['timeout'] = $timeout;
+        if (!$this->isConnected()) {
+            return;
+        }
+        $this->connection->setTimeout($timeout);
+        $this->connection->setOptions($this->options);
+    }
+
+    /**
+     * Set fragmentation size.
+     * @param int $fragment_size Fragment size in bytes.
+     * @return self.
+     */
+    public function setFragmentSize(int $fragment_size): self
+    {
+        $this->options['fragment_size'] = $fragment_size;
+        $this->connection->setOptions($this->options);
+        return $this;
+    }
+
+    /**
+     * Get fragmentation size.
+     * @return int $fragment_size Fragment size in bytes.
+     */
+    public function getFragmentSize(): int
+    {
+        return $this->options['fragment_size'];
+    }
+
+
+    /* ---------- Connection operations ---------------------------------------------- */
+
+    /**
+     * Send text message.
+     * @param string $payload Content as string.
+     */
+    public function text(string $payload): void
+    {
+        $this->send($payload);
+    }
+
+    /**
+     * Send binary message.
+     * @param string $payload Content as binary string.
+     */
+    public function binary(string $payload): void
+    {
+        $this->send($payload, 'binary');
+    }
+
+    /**
+     * Send ping.
+     * @param string $payload Optional text as string.
+     */
+    public function ping(string $payload = ''): void
+    {
+        $this->send($payload, 'ping');
+    }
+
+    /**
+     * Send unsolicited pong.
+     * @param string $payload Optional text as string.
+     */
+    public function pong(string $payload = ''): void
+    {
+        $this->send($payload, 'pong');
+    }
+
+    /**
+     * Send message.
+     * @param string $payload Message to send.
+     * @param string $opcode Opcode to use, default: 'text'.
+     * @param bool $masked If message should be masked default: true.
+     */
+    public function send(string $payload, string $opcode = 'text', bool $masked = true): void
+    {
+        if (!$this->isConnected()) {
+            $this->connect();
+        }
+
+        if (!in_array($opcode, array_keys(self::$opcodes))) {
+            $warning = "Bad opcode '{$opcode}'.  Try 'text' or 'binary'.";
+            $this->logger->warning($warning);
+            throw new BadOpcodeException($warning);
+        }
+
+        $factory = new Factory();
+        $message = $factory->create($opcode, $payload);
+        $this->connection->pushMessage($message, $masked);
+    }
+
+    /**
+     * Tell the socket to close.
+     * @param integer $status  http://tools.ietf.org/html/rfc6455#section-7.4
+     * @param string  $message A closing message, max 125 bytes.
+     */
+    public function close(int $status = 1000, string $message = 'ttfn'): void
+    {
+        if (!$this->isConnected()) {
+            return;
+        }
+        $this->connection->close($status, $message);
+    }
+
+    /**
+     * Disconnect from server.
+     */
+    public function disconnect(): void
+    {
+        if ($this->isConnected()) {
+            $this->connection->disconnect();
+        }
+    }
+
+    /**
+     * Receive message.
+     * Note that this operation will block reading.
+     * @return mixed Message, text or null depending on settings.
+     */
+    public function receive()
+    {
+        $filter = $this->options['filter'];
+        $return_obj = $this->options['return_obj'];
+
+        if (!$this->isConnected()) {
+            $this->connect();
+        }
+
+        while (true) {
+            $message = $this->connection->pullMessage();
+            $opcode = $message->getOpcode();
+            if (in_array($opcode, $filter)) {
+                $this->last_opcode = $opcode;
+                $return = $return_obj ? $message : $message->getContent();
+                break;
+            } elseif ($opcode == 'close') {
+                $this->last_opcode = null;
+                $return = $return_obj ? $message : null;
+                break;
+            }
+        }
+        return $return;
+    }
+
+
+    /* ---------- Connection functions ----------------------------------------------- */
+
+    /**
+     * Get last received opcode.
+     * @return string|null Opcode.
+     */
+    public function getLastOpcode(): ?string
+    {
+        return $this->last_opcode;
+    }
+
+    /**
+     * Get close status on connection.
+     * @return int|null Close status.
+     */
+    public function getCloseStatus(): ?int
+    {
+        return $this->connection ? $this->connection->getCloseStatus() : null;
+    }
+
+    /**
+     * If Client has active connection.
+     * @return bool True if active connection.
+     */
+    public function isConnected(): bool
+    {
+        return $this->connection && $this->connection->isConnected();
+    }
+
+    /**
+     * Get name of local socket, or null if not connected.
+     * @return string|null
+     */
+    public function getName(): ?string
+    {
+        return $this->isConnected() ? $this->connection->getName() : null;
+    }
+
+    /**
+     * Get name of remote socket, or null if not connected.
+     * @return string|null
+     */
+    public function getRemoteName(): ?string
+    {
+        return $this->isConnected() ? $this->connection->getRemoteName() : null;
+    }
+
+    /**
+     * Get name of remote socket, or null if not connected.
+     * @return string|null
+     * @deprecated Will be removed in future version, use getPeer() instead.
+     */
+    public function getPier(): ?string
+    {
+        trigger_error(
+            'getPier() is deprecated and will be removed in future version. Use getRemoteName() instead.',
+            E_USER_DEPRECATED
+        );
+        return $this->getRemoteName();
+    }
+
+
+    /* ---------- Helper functions --------------------------------------------------- */
+
+    /**
+     * Perform WebSocket handshake
+     */
+    protected function connect(): void
+    {
+        $this->connection = null;
+
+        $host_uri = $this->socket_uri
+            ->withScheme($this->socket_uri->getScheme() == 'wss' ? 'ssl' : 'tcp')
+            ->withPort($this->socket_uri->getPort() ?? ($this->socket_uri->getScheme() == 'wss' ? 443 : 80))
+            ->withPath('')
+            ->withQuery('')
+            ->withFragment('')
+            ->withUserInfo('');
+
+        // Path must be absolute
+        $http_path = $this->socket_uri->getPath();
+        if ($http_path === '' || $http_path[0] !== '/') {
+            $http_path = "/{$http_path}";
+        }
+
+        $http_uri = (new Uri())
+            ->withPath($http_path)
+            ->withQuery($this->socket_uri->getQuery());
+
+        // Set the stream context options if they're already set in the config
+        if (isset($this->options['context'])) {
+            // Suppress the error since we'll catch it below
+            if (@get_resource_type($this->options['context']) === 'stream-context') {
+                $context = $this->options['context'];
+            } else {
+                $error = "Stream context in \$options['context'] isn't a valid context.";
+                $this->logger->error($error);
+                throw new \InvalidArgumentException($error);
+            }
+        } else {
+            $context = stream_context_create();
+        }
+
+        $persistent = $this->options['persistent'] === true;
+        $flags = STREAM_CLIENT_CONNECT;
+        $flags = $persistent ? $flags | STREAM_CLIENT_PERSISTENT : $flags;
+        $socket = null;
+
+        try {
+            $handler = new ErrorHandler();
+            $socket = $handler->with(function () use ($host_uri, $flags, $context) {
+                $error = $errno = $errstr = null;
+                // Open the socket.
+                return stream_socket_client(
+                    $host_uri,
+                    $errno,
+                    $errstr,
+                    $this->options['timeout'],
+                    $flags,
+                    $context
+                );
+            });
+            if (!$socket) {
+                throw new ErrorException('No socket');
+            }
+        } catch (ErrorException $e) {
+            $error = "Could not open socket to \"{$host_uri->getAuthority()}\": {$e->getMessage()} ({$e->getCode()}).";
+            $this->logger->error($error, ['severity' => $e->getSeverity()]);
+            throw new ConnectionException($error, 0, [], $e);
+        }
+
+        $this->connection = new Connection($socket, $this->options);
+        $this->connection->setLogger($this->logger);
+        if (!$this->isConnected()) {
+            $error = "Invalid stream on \"{$host_uri->getAuthority()}\".";
+            $this->logger->error($error);
+            throw new ConnectionException($error);
+        }
+
+        if (!$persistent || $this->connection->tell() == 0) {
+            // Set timeout on the stream as well.
+            $this->connection->setTimeout($this->options['timeout']);
+
+            // Generate the WebSocket key.
+            $key = self::generateKey();
+
+            // Default headers
+            $headers = [
+                'Host'                  => $host_uri->getAuthority(),
+                'User-Agent'            => 'websocket-client-php',
+                'Connection'            => 'Upgrade',
+                'Upgrade'               => 'websocket',
+                'Sec-WebSocket-Key'     => $key,
+                'Sec-WebSocket-Version' => '13',
+            ];
+
+            // Handle basic authentication.
+            if ($userinfo = $this->socket_uri->getUserInfo()) {
+                $headers['authorization'] = 'Basic ' . base64_encode($userinfo);
+            }
+
+            // Deprecated way of adding origin (use headers instead).
+            if (isset($this->options['origin'])) {
+                $headers['origin'] = $this->options['origin'];
+            }
+
+            // Add and override with headers from options.
+            if (isset($this->options['headers'])) {
+                $headers = array_merge($headers, $this->options['headers']);
+            }
+
+            $header = "GET {$http_uri} HTTP/1.1\r\n" . implode(
+                "\r\n",
+                array_map(
+                    function ($key, $value) {
+                        return "$key: $value";
+                    },
+                    array_keys($headers),
+                    $headers
+                )
+            ) . "\r\n\r\n";
+
+            // Send headers.
+            $this->connection->write($header);
+
+            // Get server response header (terminated with double CR+LF).
+            $response = '';
+            try {
+                do {
+                    $buffer = $this->connection->gets(1024);
+                    $response .= $buffer;
+                } while (substr_count($response, "\r\n\r\n") == 0);
+            } catch (Exception $e) {
+                throw new ConnectionException('Client handshake error', $e->getCode(), $e->getData(), $e);
+            }
+
+            // Validate response.
+            if (!preg_match('#Sec-WebSocket-Accept:\s(.*)$#mUi', $response, $matches)) {
+                $error = sprintf(
+                    "Connection to '%s' failed: Server sent invalid upgrade response: %s",
+                    (string)$this->socket_uri,
+                    (string)$response
+                );
+                $this->logger->error($error);
+                throw new ConnectionException($error);
+            }
+
+            $keyAccept = trim($matches[1]);
+            $expectedResonse = base64_encode(
+                pack('H*', sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'))
+            );
+
+            if ($keyAccept !== $expectedResonse) {
+                $error = 'Server sent bad upgrade response.';
+                $this->logger->error($error);
+                throw new ConnectionException($error);
+            }
+        }
+
+        $this->logger->info("Client connected to {$this->socket_uri}");
+    }
+
+    /**
+     * Generate a random string for WebSocket key.
+     * @return string Random string
+     */
+    protected static function generateKey(): string
+    {
+        $key = '';
+        for ($i = 0; $i < 16; $i++) {
+            $key .= chr(rand(33, 126));
+        }
+        return base64_encode($key);
+    }
+
+    protected function parseUri($uri): UriInterface
+    {
+        if ($uri instanceof UriInterface) {
+            $uri = $uri;
+        } elseif (is_string($uri)) {
+            try {
+                $uri = new Uri($uri);
+            } catch (InvalidArgumentException $e) {
+                throw new BadUriException("Invalid URI '{$uri}' provided.", 0, $e);
+            }
+        } else {
+            throw new BadUriException("Provided URI must be a UriInterface or string.");
+        }
+        if (!in_array($uri->getScheme(), ['ws', 'wss'])) {
+            throw new BadUriException("Invalid URI scheme, must be 'ws' or 'wss'.");
+        }
+        return $uri;
+    }
+}

+ 518 - 0
vendor/textalk/websocket/lib/Connection.php

@@ -0,0 +1,518 @@
+<?php
+
+/**
+ * Copyright (C) 2014-2022 Textalk/Abicart and contributors.
+ *
+ * This file is part of Websocket PHP and is free software under the ISC License.
+ * License text: https://raw.githubusercontent.com/Textalk/websocket-php/master/COPYING
+ */
+
+namespace WebSocket;
+
+use Psr\Log\{
+    LoggerAwareInterface,
+    LoggerAwareTrait,
+    LoggerInterface, NullLogger
+};
+use WebSocket\Message\{
+    Factory,
+    Message
+};
+
+class Connection implements LoggerAwareInterface
+{
+    use LoggerAwareTrait;
+    use OpcodeTrait;
+
+    private $stream;
+    private $read_buffer;
+    private $msg_factory;
+    private $options = [];
+
+    protected $is_closing = false;
+    protected $close_status = null;
+
+    private $uid;
+
+    /* ---------- Construct & Destruct ----------------------------------------------- */
+
+    public function __construct($stream, array $options = [])
+    {
+        $this->stream = $stream;
+        $this->setOptions($options);
+        $this->setLogger(new NullLogger());
+        $this->msg_factory = new Factory();
+    }
+
+    public function __destruct()
+    {
+        if ($this->getType() === 'stream') {
+            fclose($this->stream);
+        }
+    }
+
+    public function setOptions(array $options = []): void
+    {
+        $this->options = array_merge($this->options, $options);
+    }
+
+    public function getCloseStatus(): ?int
+    {
+        return $this->close_status;
+    }
+
+    /**
+     * Tell the socket to close.
+     *
+     * @param integer $status  http://tools.ietf.org/html/rfc6455#section-7.4
+     * @param string  $message A closing message, max 125 bytes.
+     */
+    public function close(int $status = 1000, string $message = 'ttfn'): void
+    {
+        if (!$this->isConnected()) {
+            return;
+        }
+        $status_binstr = sprintf('%016b', $status);
+        $status_str = '';
+        foreach (str_split($status_binstr, 8) as $binstr) {
+            $status_str .= chr(bindec($binstr));
+        }
+        $message = $this->msg_factory->create('close', $status_str . $message);
+        $this->pushMessage($message, true);
+
+        $this->logger->debug("Closing with status: {$status}.");
+
+        $this->is_closing = true;
+        while (true) {
+            $message = $this->pullMessage();
+            if ($message->getOpcode() == 'close') {
+                break;
+            }
+        }
+    }
+
+
+    /* ---------- Message methods ---------------------------------------------------- */
+
+    // Push a message to stream
+    public function pushMessage(Message $message, bool $masked = true): void
+    {
+        $frames = $message->getFrames($masked, $this->options['fragment_size']);
+        foreach ($frames as $frame) {
+            $this->pushFrame($frame);
+        }
+        $this->logger->info("[connection] Pushed {$message}", [
+            'opcode' => $message->getOpcode(),
+            'content-length' => $message->getLength(),
+            'frames' => count($frames),
+        ]);
+    }
+
+    // Pull a message from stream
+    public function pullMessage(): Message
+    {
+        do {
+            $frame = $this->pullFrame();
+            $frame = $this->autoRespond($frame);
+            list ($final, $payload, $opcode, $masked) = $frame;
+
+            if ($opcode == 'close') {
+                $this->close();
+            }
+
+            // Continuation and factual opcode
+            $continuation = $opcode == 'continuation';
+            $payload_opcode = $continuation ? $this->read_buffer['opcode'] : $opcode;
+
+            // First continuation frame, create buffer
+            if (!$final && !$continuation) {
+                $this->read_buffer = ['opcode' => $opcode, 'payload' => $payload, 'frames' => 1];
+                continue; // Continue reading
+            }
+
+            // Subsequent continuation frames, add to buffer
+            if ($continuation) {
+                $this->read_buffer['payload'] .= $payload;
+                $this->read_buffer['frames']++;
+            }
+        } while (!$final);
+
+        // Final, return payload
+        $frames = 1;
+        if ($continuation) {
+            $payload = $this->read_buffer['payload'];
+            $frames = $this->read_buffer['frames'];
+            $this->read_buffer = null;
+        }
+
+        $message = $this->msg_factory->create($payload_opcode, $payload);
+
+        $this->logger->info("[connection] Pulled {$message}", [
+            'opcode' => $payload_opcode,
+            'content-length' => strlen($payload),
+            'frames' => $frames,
+        ]);
+
+        return $message;
+    }
+
+
+    /* ---------- Frame I/O methods -------------------------------------------------- */
+
+    // Pull frame from stream
+    private function pullFrame(): array
+    {
+        // Read the fragment "header" first, two bytes.
+        $data = $this->read(2);
+        list ($byte_1, $byte_2) = array_values(unpack('C*', $data));
+        $final = (bool)($byte_1 & 0b10000000); // Final fragment marker.
+        $rsv = $byte_1 & 0b01110000; // Unused bits, ignore
+
+        // Parse opcode
+        $opcode_int = $byte_1 & 0b00001111;
+        $opcode_ints = array_flip(self::$opcodes);
+        if (!array_key_exists($opcode_int, $opcode_ints)) {
+            $warning = "Bad opcode in websocket frame: {$opcode_int}";
+            $this->logger->warning($warning);
+            throw new ConnectionException($warning, ConnectionException::BAD_OPCODE);
+        }
+        $opcode = $opcode_ints[$opcode_int];
+
+        // Masking bit
+        $masked = (bool)($byte_2 & 0b10000000);
+
+        $payload = '';
+
+        // Payload length
+        $payload_length = $byte_2 & 0b01111111;
+
+        if ($payload_length > 125) {
+            if ($payload_length === 126) {
+                $data = $this->read(2); // 126: Payload is a 16-bit unsigned int
+                $payload_length = current(unpack('n', $data));
+            } else {
+                $data = $this->read(8); // 127: Payload is a 64-bit unsigned int
+                $payload_length = current(unpack('J', $data));
+            }
+        }
+
+        // Get masking key.
+        if ($masked) {
+            $masking_key = $this->read(4);
+        }
+
+        // Get the actual payload, if any (might not be for e.g. close frames.
+        if ($payload_length > 0) {
+            $data = $this->read($payload_length);
+
+            if ($masked) {
+                // Unmask payload.
+                for ($i = 0; $i < $payload_length; $i++) {
+                    $payload .= ($data[$i] ^ $masking_key[$i % 4]);
+                }
+            } else {
+                $payload = $data;
+            }
+        }
+
+        $this->logger->debug("[connection] Pulled '{opcode}' frame", [
+            'opcode' => $opcode,
+            'final' => $final,
+            'content-length' => strlen($payload),
+        ]);
+        return [$final, $payload, $opcode, $masked];
+    }
+
+    // Push frame to stream
+    private function pushFrame(array $frame): void
+    {
+        list ($final, $payload, $opcode, $masked) = $frame;
+        $data = '';
+        $byte_1 = $final ? 0b10000000 : 0b00000000; // Final fragment marker.
+        $byte_1 |= self::$opcodes[$opcode]; // Set opcode.
+        $data .= pack('C', $byte_1);
+
+        $byte_2 = $masked ? 0b10000000 : 0b00000000; // Masking bit marker.
+
+        // 7 bits of payload length...
+        $payload_length = strlen($payload);
+        if ($payload_length > 65535) {
+            $data .= pack('C', $byte_2 | 0b01111111);
+            $data .= pack('J', $payload_length);
+        } elseif ($payload_length > 125) {
+            $data .= pack('C', $byte_2 | 0b01111110);
+            $data .= pack('n', $payload_length);
+        } else {
+            $data .= pack('C', $byte_2 | $payload_length);
+        }
+
+        // Handle masking
+        if ($masked) {
+            // generate a random mask:
+            $mask = '';
+            for ($i = 0; $i < 4; $i++) {
+                $mask .= chr(rand(0, 255));
+            }
+            $data .= $mask;
+
+            // Append payload to frame:
+            for ($i = 0; $i < $payload_length; $i++) {
+                $data .= $payload[$i] ^ $mask[$i % 4];
+            }
+        } else {
+            $data .= $payload;
+        }
+
+        $this->write($data);
+
+        $this->logger->debug("[connection] Pushed '{$opcode}' frame", [
+            'opcode' => $opcode,
+            'final' => $final,
+            'content-length' => strlen($payload),
+        ]);
+    }
+
+    // Trigger auto response for frame
+    private function autoRespond(array $frame)
+    {
+        list ($final, $payload, $opcode, $masked) = $frame;
+        $payload_length = strlen($payload);
+
+        switch ($opcode) {
+            case 'ping':
+                // If we received a ping, respond with a pong
+                $this->logger->debug("[connection] Received 'ping', sending 'pong'.");
+                $message = $this->msg_factory->create('pong', $payload);
+                $this->pushMessage($message, $masked);
+                return [$final, $payload, $opcode, $masked];
+            case 'close':
+                // If we received close, possibly acknowledge and close connection
+                $status_bin = '';
+                $status = '';
+                if ($payload_length > 0) {
+                    $status_bin = $payload[0] . $payload[1];
+                    $status = current(unpack('n', $payload));
+                    $this->close_status = $status;
+                }
+                // Get additional close message
+                if ($payload_length >= 2) {
+                    $payload = substr($payload, 2);
+                }
+
+                $this->logger->debug("[connection] Received 'close', status: {$status}.");
+                if (!$this->is_closing) {
+                    $ack =  "{$status_bin}Close acknowledged: {$status}";
+                    $message = $this->msg_factory->create('close', $ack);
+                    $this->pushMessage($message, $masked);
+                } else {
+                    $this->is_closing = false; // A close response, all done.
+                }
+                $this->disconnect();
+                return [$final, $payload, $opcode, $masked];
+            default:
+                return [$final, $payload, $opcode, $masked];
+        }
+    }
+
+
+    /* ---------- Stream I/O methods ------------------------------------------------- */
+
+    /**
+     * Close connection stream.
+     * @return bool
+     */
+    public function disconnect(): bool
+    {
+        $this->logger->debug('Closing connection');
+        return fclose($this->stream);
+    }
+
+    /**
+     * If connected to stream.
+     * @return bool
+     */
+    public function isConnected(): bool
+    {
+        return in_array($this->getType(), ['stream', 'persistent stream']);
+    }
+
+    /**
+     * Return type of connection.
+     * @return string|null Type of connection or null if invalid type.
+     */
+    public function getType(): ?string
+    {
+        return get_resource_type($this->stream);
+    }
+
+    /**
+     * Get name of local socket, or null if not connected.
+     * @return string|null
+     */
+    public function getName(): ?string
+    {
+        return stream_socket_get_name($this->stream, false);
+    }
+
+    /**
+     * Get name of remote socket, or null if not connected.
+     * @return string|null
+     */
+    public function getRemoteName(): ?string
+    {
+        return stream_socket_get_name($this->stream, true);
+    }
+
+    /**
+     * Get meta data for connection.
+     * @return array
+     */
+    public function getMeta(): array
+    {
+        return stream_get_meta_data($this->stream);
+    }
+
+    /**
+     * Returns current position of stream pointer.
+     * @return int
+     * @throws ConnectionException
+     */
+    public function tell(): int
+    {
+        $tell = ftell($this->stream);
+        if ($tell === false) {
+            $this->throwException('Could not resolve stream pointer position');
+        }
+        return $tell;
+    }
+
+    /**
+     * If stream pointer is at end of file.
+     * @return bool
+     */
+    public function eof(): int
+    {
+        return feof($this->stream);
+    }
+
+
+    /* ---------- Stream option methods ---------------------------------------------- */
+
+    /**
+     * Set time out on connection.
+     * @param int $seconds Timeout part in seconds
+     * @param int $microseconds Timeout part in microseconds
+     * @return bool
+     */
+    public function setTimeout(int $seconds, int $microseconds = 0): bool
+    {
+        $this->logger->debug("Setting timeout {$seconds}:{$microseconds} seconds");
+        return stream_set_timeout($this->stream, $seconds, $microseconds);
+    }
+
+
+    /* ---------- Stream read/write methods ------------------------------------------ */
+
+    /**
+     * Read line from stream.
+     * @param int $length Maximum number of bytes to read
+     * @param string $ending Line delimiter
+     * @return string Read data
+     */
+    public function getLine(int $length, string $ending): string
+    {
+        $line = stream_get_line($this->stream, $length, $ending);
+        if ($line === false) {
+            $this->throwException('Could not read from stream');
+        }
+        $read = strlen($line);
+        $this->logger->debug("Read {$read} bytes of line.");
+        return $line;
+    }
+
+    /**
+     * Read line from stream.
+     * @param int $length Maximum number of bytes to read
+     * @return string Read data
+     */
+    public function gets(int $length): string
+    {
+        $line = fgets($this->stream, $length);
+        if ($line === false) {
+            $this->throwException('Could not read from stream');
+        }
+        $read = strlen($line);
+        $this->logger->debug("Read {$read} bytes of line.");
+        return $line;
+    }
+
+    /**
+     * Read characters from stream.
+     * @param int $length Maximum number of bytes to read
+     * @return string Read data
+     */
+    public function read(string $length): string
+    {
+        $data = '';
+        while (strlen($data) < $length) {
+            $buffer = fread($this->stream, $length - strlen($data));
+            if (!$buffer) {
+                $meta = stream_get_meta_data($this->stream);
+                if (!empty($meta['timed_out'])) {
+                    $message = 'Client read timeout';
+                    $this->logger->error($message, $meta);
+                    throw new TimeoutException($message, ConnectionException::TIMED_OUT, $meta);
+                }
+            }
+            if ($buffer === false) {
+                $read = strlen($data);
+                $this->throwException("Broken frame, read {$read} of stated {$length} bytes.");
+            }
+            if ($buffer === '') {
+                $this->throwException("Empty read; connection dead?");
+            }
+            $data .= $buffer;
+            $read = strlen($data);
+            $this->logger->debug("Read {$read} of {$length} bytes.");
+        }
+        return $data;
+    }
+
+    /**
+     * Write characters to stream.
+     * @param string $data Data to read
+     */
+    public function write(string $data): void
+    {
+        $length = strlen($data);
+        $written = fwrite($this->stream, $data);
+        if ($written === false) {
+            $this->throwException("Failed to write {$length} bytes.");
+        }
+        if ($written < strlen($data)) {
+            $this->throwException("Could only write {$written} out of {$length} bytes.");
+        }
+        $this->logger->debug("Wrote {$written} of {$length} bytes.");
+    }
+
+
+    /* ---------- Internal helper methods -------------------------------------------- */
+
+    private function throwException(string $message, int $code = 0): void
+    {
+        $meta = ['closed' => true];
+        if ($this->isConnected()) {
+            $meta = $this->getMeta();
+            $this->disconnect();
+            if (!empty($meta['timed_out'])) {
+                $this->logger->error($message, $meta);
+                throw new TimeoutException($message, ConnectionException::TIMED_OUT, $meta);
+            }
+            if (!empty($meta['eof'])) {
+                $code = ConnectionException::EOF;
+            }
+        }
+        $this->logger->error($message, $meta);
+        throw new ConnectionException($message, $code, $meta);
+    }
+}

+ 33 - 0
vendor/textalk/websocket/lib/ConnectionException.php

@@ -0,0 +1,33 @@
+<?php
+
+/**
+ * Copyright (C) 2014-2022 Textalk/Abicart and contributors.
+ *
+ * This file is part of Websocket PHP and is free software under the ISC License.
+ * License text: https://raw.githubusercontent.com/Textalk/websocket-php/master/COPYING
+ */
+
+namespace WebSocket;
+
+use Throwable;
+
+class ConnectionException extends Exception
+{
+    // Native codes in interval 0-106
+    public const TIMED_OUT = 1024;
+    public const EOF = 1025;
+    public const BAD_OPCODE = 1026;
+
+    private $data;
+
+    public function __construct(string $message, int $code = 0, array $data = [], Throwable $prev = null)
+    {
+        parent::__construct($message, $code, $prev);
+        $this->data = $data;
+    }
+
+    public function getData(): array
+    {
+        return $this->data;
+    }
+}

+ 7 - 0
vendor/textalk/websocket/lib/Exception.php

@@ -0,0 +1,7 @@
+<?php
+
+namespace WebSocket;
+
+class Exception extends \Exception
+{
+}

+ 15 - 0
vendor/textalk/websocket/lib/Message/Binary.php

@@ -0,0 +1,15 @@
+<?php
+
+/**
+ * Copyright (C) 2014-2022 Textalk/Abicart and contributors.
+ *
+ * This file is part of Websocket PHP and is free software under the ISC License.
+ * License text: https://raw.githubusercontent.com/Textalk/websocket-php/master/COPYING
+ */
+
+namespace WebSocket\Message;
+
+class Binary extends Message
+{
+    protected $opcode = 'binary';
+}

+ 15 - 0
vendor/textalk/websocket/lib/Message/Close.php

@@ -0,0 +1,15 @@
+<?php
+
+/**
+ * Copyright (C) 2014-2022 Textalk/Abicart and contributors.
+ *
+ * This file is part of Websocket PHP and is free software under the ISC License.
+ * License text: https://raw.githubusercontent.com/Textalk/websocket-php/master/COPYING
+ */
+
+namespace WebSocket\Message;
+
+class Close extends Message
+{
+    protected $opcode = 'close';
+}

+ 32 - 0
vendor/textalk/websocket/lib/Message/Factory.php

@@ -0,0 +1,32 @@
+<?php
+
+/**
+ * Copyright (C) 2014-2022 Textalk/Abicart and contributors.
+ *
+ * This file is part of Websocket PHP and is free software under the ISC License.
+ * License text: https://raw.githubusercontent.com/Textalk/websocket-php/master/COPYING
+ */
+
+namespace WebSocket\Message;
+
+use WebSocket\BadOpcodeException;
+
+class Factory
+{
+    public function create(string $opcode, string $payload = ''): Message
+    {
+        switch ($opcode) {
+            case 'text':
+                return new Text($payload);
+            case 'binary':
+                return new Binary($payload);
+            case 'ping':
+                return new Ping($payload);
+            case 'pong':
+                return new Pong($payload);
+            case 'close':
+                return new Close($payload);
+        }
+        throw new BadOpcodeException("Invalid opcode '{$opcode}' provided");
+    }
+}

+ 74 - 0
vendor/textalk/websocket/lib/Message/Message.php

@@ -0,0 +1,74 @@
+<?php
+
+/**
+ * Copyright (C) 2014-2022 Textalk/Abicart and contributors.
+ *
+ * This file is part of Websocket PHP and is free software under the ISC License.
+ * License text: https://raw.githubusercontent.com/Textalk/websocket-php/master/COPYING
+ */
+
+namespace WebSocket\Message;
+
+use DateTime;
+
+abstract class Message
+{
+    protected $opcode;
+    protected $payload;
+    protected $timestamp;
+
+    public function __construct(string $payload = '')
+    {
+        $this->payload = $payload;
+        $this->timestamp = new DateTime();
+    }
+
+    public function getOpcode(): string
+    {
+        return $this->opcode;
+    }
+
+    public function getLength(): int
+    {
+        return strlen($this->payload);
+    }
+
+    public function getTimestamp(): DateTime
+    {
+        return $this->timestamp;
+    }
+
+    public function getContent(): string
+    {
+        return $this->payload;
+    }
+
+    public function setContent(string $payload = ''): void
+    {
+        $this->payload = $payload;
+    }
+
+    public function hasContent(): bool
+    {
+        return $this->payload != '';
+    }
+
+    public function __toString(): string
+    {
+        return get_class($this);
+    }
+
+    // Split messages into frames
+    public function getFrames(bool $masked = true, int $framesize = 4096): array
+    {
+
+        $frames = [];
+        $split = str_split($this->getContent(), $framesize) ?: [''];
+        foreach ($split as $payload) {
+            $frames[] = [false, $payload, 'continuation', $masked];
+        }
+        $frames[0][2] = $this->opcode;
+        $frames[array_key_last($frames)][0] = true;
+        return $frames;
+    }
+}

+ 15 - 0
vendor/textalk/websocket/lib/Message/Ping.php

@@ -0,0 +1,15 @@
+<?php
+
+/**
+ * Copyright (C) 2014-2022 Textalk/Abicart and contributors.
+ *
+ * This file is part of Websocket PHP and is free software under the ISC License.
+ * License text: https://raw.githubusercontent.com/Textalk/websocket-php/master/COPYING
+ */
+
+namespace WebSocket\Message;
+
+class Ping extends Message
+{
+    protected $opcode = 'ping';
+}

+ 15 - 0
vendor/textalk/websocket/lib/Message/Pong.php

@@ -0,0 +1,15 @@
+<?php
+
+/**
+ * Copyright (C) 2014-2022 Textalk/Abicart and contributors.
+ *
+ * This file is part of Websocket PHP and is free software under the ISC License.
+ * License text: https://raw.githubusercontent.com/Textalk/websocket-php/master/COPYING
+ */
+
+namespace WebSocket\Message;
+
+class Pong extends Message
+{
+    protected $opcode = 'pong';
+}

+ 15 - 0
vendor/textalk/websocket/lib/Message/Text.php

@@ -0,0 +1,15 @@
+<?php
+
+/**
+ * Copyright (C) 2014-2022 Textalk/Abicart and contributors.
+ *
+ * This file is part of Websocket PHP and is free software under the ISC License.
+ * License text: https://raw.githubusercontent.com/Textalk/websocket-php/master/COPYING
+ */
+
+namespace WebSocket\Message;
+
+class Text extends Message
+{
+    protected $opcode = 'text';
+}

+ 22 - 0
vendor/textalk/websocket/lib/OpcodeTrait.php

@@ -0,0 +1,22 @@
+<?php
+
+/**
+ * Copyright (C) 2014-2022 Textalk/Abicart and contributors.
+ *
+ * This file is part of Websocket PHP and is free software under the ISC License.
+ * License text: https://raw.githubusercontent.com/Textalk/websocket-php/master/COPYING
+ */
+
+namespace WebSocket;
+
+trait OpcodeTrait
+{
+    private static $opcodes = [
+        'continuation' => 0,
+        'text'         => 1,
+        'binary'       => 2,
+        'close'        => 8,
+        'ping'         => 9,
+        'pong'         => 10,
+    ];
+}

+ 470 - 0
vendor/textalk/websocket/lib/Server.php

@@ -0,0 +1,470 @@
+<?php
+
+/**
+ * Copyright (C) 2014-2022 Textalk/Abicart and contributors.
+ *
+ * This file is part of Websocket PHP and is free software under the ISC License.
+ * License text: https://raw.githubusercontent.com/Textalk/websocket-php/master/COPYING
+ */
+
+namespace WebSocket;
+
+use Closure;
+use ErrorException;
+use Phrity\Util\ErrorHandler;
+use Psr\Log\{
+    LoggerAwareInterface,
+    LoggerAwareTrait,
+    LoggerInterface,
+    NullLogger
+};
+use Throwable;
+use WebSocket\Message\Factory;
+
+class Server implements LoggerAwareInterface
+{
+    use LoggerAwareTrait; // Provides setLogger(LoggerInterface $logger)
+    use OpcodeTrait;
+
+    // Default options
+    protected static $default_options = [
+        'filter'        => ['text', 'binary'],
+        'fragment_size' => 4096,
+        'logger'        => null,
+        'port'          => 8000,
+        'return_obj'    => false,
+        'timeout'       => null,
+    ];
+
+    protected $port;
+    protected $listening;
+    protected $request;
+    protected $request_path;
+    private $connections = [];
+    private $options = [];
+    private $listen = false;
+    private $last_opcode;
+
+
+    /* ---------- Magic methods ------------------------------------------------------ */
+
+    /**
+     * @param array $options
+     *   Associative array containing:
+     *   - filter:        Array of opcodes to handle. Default: ['text', 'binary'].
+     *   - fragment_size: Set framgemnt size.  Default: 4096
+     *   - logger:        PSR-3 compatible logger.  Default NullLogger.
+     *   - port:          Chose port for listening.  Default 8000.
+     *   - return_obj:    If receive() function return Message instance.  Default false.
+     *   - timeout:       Set the socket timeout in seconds.
+     */
+    public function __construct(array $options = [])
+    {
+        $this->options = array_merge(self::$default_options, [
+            'logger' => new NullLogger(),
+        ], $options);
+        $this->port = $this->options['port'];
+        $this->setLogger($this->options['logger']);
+
+        $error = $errno = $errstr = null;
+        set_error_handler(function (int $severity, string $message, string $file, int $line) use (&$error) {
+            $this->logger->warning($message, ['severity' => $severity]);
+            $error = $message;
+        }, E_ALL);
+
+        do {
+            $this->listening = stream_socket_server("tcp://0.0.0.0:$this->port", $errno, $errstr);
+        } while ($this->listening === false && $this->port++ < 10000);
+
+        restore_error_handler();
+
+        if (!$this->listening) {
+            $error = "Could not open listening socket: {$errstr} ({$errno}) {$error}";
+            $this->logger->error($error);
+            throw new ConnectionException($error, (int)$errno);
+        }
+
+        $this->logger->info("Server listening to port {$this->port}");
+    }
+
+    /**
+     * Get string representation of instance.
+     * @return string String representation.
+     */
+    public function __toString(): string
+    {
+        return sprintf(
+            "%s(%s)",
+            get_class($this),
+            $this->getName() ?: 'closed'
+        );
+    }
+
+
+    /* ---------- Server operations -------------------------------------------------- */
+
+    /**
+     * Accept a single incoming request.
+     * Note that this operation will block accepting additional requests.
+     * @return bool True if listening.
+     */
+    public function accept(): bool
+    {
+        $this->disconnect();
+        return (bool)$this->listening;
+    }
+
+
+    /* ---------- Server option functions -------------------------------------------- */
+
+    /**
+     * Get current port.
+     * @return int port.
+     */
+    public function getPort(): int
+    {
+        return $this->port;
+    }
+
+    /**
+     * Set timeout.
+     * @param int $timeout Timeout in seconds.
+     */
+    public function setTimeout(int $timeout): void
+    {
+        $this->options['timeout'] = $timeout;
+        if (!$this->isConnected()) {
+            return;
+        }
+        foreach ($this->connections as $connection) {
+            $connection->setTimeout($timeout);
+            $connection->setOptions($this->options);
+        }
+    }
+
+    /**
+     * Set fragmentation size.
+     * @param int $fragment_size Fragment size in bytes.
+     * @return self.
+     */
+    public function setFragmentSize(int $fragment_size): self
+    {
+        $this->options['fragment_size'] = $fragment_size;
+        foreach ($this->connections as $connection) {
+            $connection->setOptions($this->options);
+        }
+        return $this;
+    }
+
+    /**
+     * Get fragmentation size.
+     * @return int $fragment_size Fragment size in bytes.
+     */
+    public function getFragmentSize(): int
+    {
+        return $this->options['fragment_size'];
+    }
+
+
+    /* ---------- Connection broadcast operations ------------------------------------ */
+
+    /**
+     * Broadcast text message to all conenctions.
+     * @param string $payload Content as string.
+     */
+    public function text(string $payload): void
+    {
+        $this->send($payload);
+    }
+
+    /**
+     * Broadcast binary message to all conenctions.
+     * @param string $payload Content as binary string.
+     */
+    public function binary(string $payload): void
+    {
+        $this->send($payload, 'binary');
+    }
+
+    /**
+     * Broadcast ping message to all conenctions.
+     * @param string $payload Optional text as string.
+     */
+    public function ping(string $payload = ''): void
+    {
+        $this->send($payload, 'ping');
+    }
+
+    /**
+     * Broadcast pong message to all conenctions.
+     * @param string $payload Optional text as string.
+     */
+    public function pong(string $payload = ''): void
+    {
+        $this->send($payload, 'pong');
+    }
+
+    /**
+     * Send message on all connections.
+     * @param string $payload Message to send.
+     * @param string $opcode Opcode to use, default: 'text'.
+     * @param bool $masked If message should be masked default: true.
+     */
+    public function send(string $payload, string $opcode = 'text', bool $masked = true): void
+    {
+        if (!$this->isConnected()) {
+            $this->connect();
+        }
+        if (!in_array($opcode, array_keys(self::$opcodes))) {
+            $warning = "Bad opcode '{$opcode}'.  Try 'text' or 'binary'.";
+            $this->logger->warning($warning);
+            throw new BadOpcodeException($warning);
+        }
+
+        $factory = new Factory();
+        $message = $factory->create($opcode, $payload);
+
+        foreach ($this->connections as $connection) {
+            $connection->pushMessage($message, $masked);
+        }
+    }
+
+    /**
+     * Close all connections.
+     * @param int $status Close status, default: 1000.
+     * @param string $message Close message, default: 'ttfn'.
+     */
+    public function close(int $status = 1000, string $message = 'ttfn'): void
+    {
+        foreach ($this->connections as $connection) {
+            if ($connection->isConnected()) {
+                $connection->close($status, $message);
+            }
+        }
+    }
+
+    /**
+     * Disconnect all connections.
+     */
+    public function disconnect(): void
+    {
+        foreach ($this->connections as $connection) {
+            if ($connection->isConnected()) {
+                $connection->disconnect();
+            }
+        }
+        $this->connections = [];
+    }
+
+    /**
+     * Receive message from single connection.
+     * Note that this operation will block reading and only read from first available connection.
+     * @return mixed Message, text or null depending on settings.
+     */
+    public function receive()
+    {
+        $filter = $this->options['filter'];
+        $return_obj = $this->options['return_obj'];
+
+        if (!$this->isConnected()) {
+            $this->connect();
+        }
+        $connection = current($this->connections);
+
+        while (true) {
+            $message = $connection->pullMessage();
+            $opcode = $message->getOpcode();
+            if (in_array($opcode, $filter)) {
+                $this->last_opcode = $opcode;
+                $return = $return_obj ? $message : $message->getContent();
+                break;
+            } elseif ($opcode == 'close') {
+                $this->last_opcode = null;
+                $return = $return_obj ? $message : null;
+                break;
+            }
+        }
+        return $return;
+    }
+
+
+    /* ---------- Connection functions ----------------------------------------------- */
+
+    /**
+     * Get requested path from last connection.
+     * @return string Path.
+     */
+    public function getPath(): string
+    {
+        return $this->request_path;
+    }
+
+    /**
+     * Get request from last connection.
+     * @return array Request.
+     */
+    public function getRequest(): array
+    {
+        return $this->request;
+    }
+
+    /**
+     * Get headers from last connection.
+     * @return string|null Headers.
+     */
+    public function getHeader($header): ?string
+    {
+        foreach ($this->request as $row) {
+            if (stripos($row, $header) !== false) {
+                list($headername, $headervalue) = explode(":", $row);
+                return trim($headervalue);
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Get last received opcode.
+     * @return string|null Opcode.
+     */
+    public function getLastOpcode(): ?string
+    {
+        return $this->last_opcode;
+    }
+
+    /**
+     * Get close status from single connection.
+     * @return int|null Close status.
+     */
+    public function getCloseStatus(): ?int
+    {
+        return $this->connections ? current($this->connections)->getCloseStatus() : null;
+    }
+
+    /**
+     * If Server has active connections.
+     * @return bool True if active connection.
+     */
+    public function isConnected(): bool
+    {
+        foreach ($this->connections as $connection) {
+            if ($connection->isConnected()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Get name of local socket from single connection.
+     * @return string|null Name of local socket.
+     */
+    public function getName(): ?string
+    {
+        return $this->isConnected() ? current($this->connections)->getName() : null;
+    }
+
+    /**
+     * Get name of remote socket from single connection.
+     * @return string|null Name of remote socket.
+     */
+    public function getRemoteName(): ?string
+    {
+        return $this->isConnected() ? current($this->connections)->getRemoteName() : null;
+    }
+
+    /**
+     * @deprecated Will be removed in future version.
+     */
+    public function getPier(): ?string
+    {
+        trigger_error(
+            'getPier() is deprecated and will be removed in future version. Use getRemoteName() instead.',
+            E_USER_DEPRECATED
+        );
+        return $this->getRemoteName();
+    }
+
+
+    /* ---------- Helper functions --------------------------------------------------- */
+
+    // Connect when read/write operation is performed.
+    private function connect(): void
+    {
+        try {
+            $handler = new ErrorHandler();
+            $socket = $handler->with(function () {
+                if (isset($this->options['timeout'])) {
+                    $socket = stream_socket_accept($this->listening, $this->options['timeout']);
+                } else {
+                    $socket = stream_socket_accept($this->listening);
+                }
+                if (!$socket) {
+                    throw new ErrorException('No socket');
+                }
+                return $socket;
+            });
+        } catch (ErrorException $e) {
+            $error = "Server failed to connect. {$e->getMessage()}";
+            $this->logger->error($error, ['severity' => $e->getSeverity()]);
+            throw new ConnectionException($error, 0, [], $e);
+        }
+
+        $connection = new Connection($socket, $this->options);
+        $connection->setLogger($this->logger);
+
+        if (isset($this->options['timeout'])) {
+            $connection->setTimeout($this->options['timeout']);
+        }
+
+        $this->logger->info("Client has connected to port {port}", [
+            'port' => $this->port,
+            'peer' => $connection->getRemoteName(),
+        ]);
+        $this->performHandshake($connection);
+        $this->connections = ['*' => $connection];
+    }
+
+    // Perform upgrade handshake on new connections.
+    private function performHandshake(Connection $connection): void
+    {
+        $request = '';
+        do {
+            $buffer = $connection->getLine(1024, "\r\n");
+            $request .= $buffer . "\n";
+            $metadata = $connection->getMeta();
+        } while (!$connection->eof() && $metadata['unread_bytes'] > 0);
+
+        if (!preg_match('/GET (.*) HTTP\//mUi', $request, $matches)) {
+            $error = "No GET in request: {$request}";
+            $this->logger->error($error);
+            throw new ConnectionException($error);
+        }
+        $get_uri = trim($matches[1]);
+        $uri_parts = parse_url($get_uri);
+
+        $this->request = explode("\n", $request);
+        $this->request_path = $uri_parts['path'];
+        /// @todo Get query and fragment as well.
+
+        if (!preg_match('#Sec-WebSocket-Key:\s(.*)$#mUi', $request, $matches)) {
+            $error = "Client had no Key in upgrade request: {$request}";
+            $this->logger->error($error);
+            throw new ConnectionException($error);
+        }
+
+        $key = trim($matches[1]);
+
+        /// @todo Validate key length and base 64...
+        $response_key = base64_encode(pack('H*', sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')));
+
+        $header = "HTTP/1.1 101 Switching Protocols\r\n"
+                . "Upgrade: websocket\r\n"
+                . "Connection: Upgrade\r\n"
+                . "Sec-WebSocket-Accept: $response_key\r\n"
+                . "\r\n";
+
+        $connection->write($header);
+        $this->logger->debug("Handshake on {$get_uri}");
+    }
+}

+ 14 - 0
vendor/textalk/websocket/lib/TimeoutException.php

@@ -0,0 +1,14 @@
+<?php
+
+/**
+ * Copyright (C) 2014-2022 Textalk/Abicart and contributors.
+ *
+ * This file is part of Websocket PHP and is free software under the ISC License.
+ * License text: https://raw.githubusercontent.com/Textalk/websocket-php/master/COPYING
+ */
+
+namespace WebSocket;
+
+class TimeoutException extends ConnectionException
+{
+}

+ 13 - 0
vendor/textalk/websocket/phpunit.xml.dist

@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" bootstrap="tests/bootstrap.php" colors="true" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd">
+  <coverage>
+    <include>
+      <directory suffix=".php">lib/</directory>
+    </include>
+  </coverage>
+  <testsuites>
+    <testsuite name="Unit tests">
+      <directory suffix=".php">tests</directory>
+    </testsuite>
+  </testsuites>
+</phpunit>

+ 568 - 0
vendor/textalk/websocket/tests/ClientTest.php

@@ -0,0 +1,568 @@
+<?php
+
+/**
+ * Test case for Client.
+ * Note that this test is performed by mocking socket/stream calls.
+ */
+
+declare(strict_types=1);
+
+namespace WebSocket;
+
+use ErrorException;
+use Phrity\Net\Uri;
+use Phrity\Util\ErrorHandler;
+use PHPUnit\Framework\TestCase;
+
+class ClientTest extends TestCase
+{
+    public function setUp(): void
+    {
+        error_reporting(-1);
+    }
+
+    public function testClientMasked(): void
+    {
+        MockSocket::initialize('client.connect', $this);
+        $client = new Client('ws://localhost:8000/my/mock/path');
+        $client->send('Connect');
+        $this->assertTrue(MockSocket::isEmpty());
+        $this->assertEquals(4096, $client->getFragmentSize());
+
+        MockSocket::initialize('send-receive', $this);
+        $client->send('Sending a message');
+        $message = $client->receive();
+        $this->assertTrue(MockSocket::isEmpty());
+        $this->assertEquals('text', $client->getLastOpcode());
+
+        MockSocket::initialize('client.close', $this);
+        $this->assertTrue($client->isConnected());
+        $this->assertNull($client->getCloseStatus());
+
+        $client->close();
+        $this->assertFalse($client->isConnected());
+        $this->assertEquals(1000, $client->getCloseStatus());
+
+        $this->assertTrue(MockSocket::isEmpty());
+    }
+
+    public function testDestruct(): void
+    {
+        MockSocket::initialize('client.connect', $this);
+        $client = new Client('ws://localhost:8000/my/mock/path');
+        $client->send('Connect');
+        $this->assertTrue(MockSocket::isEmpty());
+
+        MockSocket::initialize('client.destruct', $this);
+    }
+
+    public function testClienExtendedUrl(): void
+    {
+        MockSocket::initialize('client.connect-extended', $this);
+        $client = new Client('ws://localhost:8000/my/mock/path?my_query=yes#my_fragment');
+        $client->send('Connect');
+        $this->assertTrue(MockSocket::isEmpty());
+    }
+
+    public function testClientNoPath(): void
+    {
+        MockSocket::initialize('client.connect-root', $this);
+        $client = new Client('ws://localhost:8000');
+        $client->send('Connect');
+        $this->assertTrue(MockSocket::isEmpty());
+    }
+
+    public function testClientRelativePath(): void
+    {
+        MockSocket::initialize('client.connect', $this);
+        $uri = new Uri('ws://localhost:8000');
+        $uri = $uri->withPath('my/mock/path');
+        $client = new Client($uri);
+        $client->send('Connect');
+        $this->assertTrue(MockSocket::isEmpty());
+    }
+
+    public function testClientWsDefaultPort(): void
+    {
+        MockSocket::initialize('client.connect-default-port-ws', $this);
+        $uri = new Uri('ws://localhost');
+        $uri = $uri->withPath('my/mock/path');
+        $client = new Client($uri);
+        $client->send('Connect');
+        $this->assertTrue(MockSocket::isEmpty());
+    }
+
+    public function testClientWssDefaultPort(): void
+    {
+        MockSocket::initialize('client.connect-default-port-wss', $this);
+        $uri = new Uri('wss://localhost');
+        $uri = $uri->withPath('my/mock/path');
+        $client = new Client($uri);
+        $client->send('Connect');
+        $this->assertTrue(MockSocket::isEmpty());
+    }
+
+    public function testClientWithTimeout(): void
+    {
+        MockSocket::initialize('client.connect-timeout', $this);
+        $client = new Client('ws://localhost:8000/my/mock/path', ['timeout' => 300]);
+        $client->send('Connect');
+        $this->assertTrue(MockSocket::isEmpty());
+    }
+
+    public function testClientWithContext(): void
+    {
+        MockSocket::initialize('client.connect-context', $this);
+        $client = new Client('ws://localhost:8000/my/mock/path', ['context' => '@mock-stream-context']);
+        $client->send('Connect');
+        $this->assertTrue(MockSocket::isEmpty());
+    }
+
+    public function testClientAuthed(): void
+    {
+        MockSocket::initialize('client.connect-authed', $this);
+        $client = new Client('wss://usename:password@localhost:8000/my/mock/path');
+        $client->send('Connect');
+        $this->assertTrue(MockSocket::isEmpty());
+    }
+
+    public function testWithHeaders(): void
+    {
+        MockSocket::initialize('client.connect-headers', $this);
+        $client = new Client('ws://localhost:8000/my/mock/path', [
+            'origin' => 'Origin header',
+            'headers' => ['Generic header' => 'Generic content'],
+        ]);
+        $client->send('Connect');
+        $this->assertTrue(MockSocket::isEmpty());
+    }
+
+    public function testPayload128(): void
+    {
+        MockSocket::initialize('client.connect', $this);
+        $client = new Client('ws://localhost:8000/my/mock/path');
+        $client->send('Connect');
+        $this->assertTrue(MockSocket::isEmpty());
+
+        $payload = file_get_contents(__DIR__ . '/mock/payload.128.txt');
+
+        MockSocket::initialize('send-receive-128', $this);
+        $client->send($payload, 'text', false);
+        $message = $client->receive();
+        $this->assertEquals($payload, $message);
+        $this->assertTrue(MockSocket::isEmpty());
+    }
+
+    public function testPayload65536(): void
+    {
+        MockSocket::initialize('client.connect', $this);
+        $client = new Client('ws://localhost:8000/my/mock/path');
+        $client->send('Connect');
+        $this->assertTrue(MockSocket::isEmpty());
+
+        $payload = file_get_contents(__DIR__ . '/mock/payload.65536.txt');
+        $client->setFragmentSize(65540);
+
+        MockSocket::initialize('send-receive-65536', $this);
+        $client->send($payload, 'text', false);
+        $message = $client->receive();
+        $this->assertEquals($payload, $message);
+        $this->assertTrue(MockSocket::isEmpty());
+        $this->assertEquals(65540, $client->getFragmentSize());
+    }
+
+    public function testMultiFragment(): void
+    {
+        MockSocket::initialize('client.connect', $this);
+        $client = new Client('ws://localhost:8000/my/mock/path');
+        $client->send('Connect');
+        $this->assertTrue(MockSocket::isEmpty());
+
+        MockSocket::initialize('send-receive-multi-fragment', $this);
+        $client->setFragmentSize(8);
+        $client->send('Multi fragment test');
+        $message = $client->receive();
+        $this->assertEquals('Multi fragment test', $message);
+        $this->assertTrue(MockSocket::isEmpty());
+        $this->assertEquals(8, $client->getFragmentSize());
+    }
+
+    public function testPingPong(): void
+    {
+        MockSocket::initialize('client.connect', $this);
+        $client = new Client('ws://localhost:8000/my/mock/path');
+        $client->send('Connect');
+        $this->assertTrue(MockSocket::isEmpty());
+
+        MockSocket::initialize('ping-pong', $this);
+        $client->send('Server ping', 'ping');
+        $client->send('', 'ping');
+        $message = $client->receive();
+        $this->assertEquals('Receiving a message', $message);
+        $this->assertEquals('text', $client->getLastOpcode());
+        $this->assertTrue(MockSocket::isEmpty());
+    }
+
+    public function testRemoteClose(): void
+    {
+        MockSocket::initialize('client.connect', $this);
+        $client = new Client('ws://localhost:8000/my/mock/path');
+        $client->send('Connect');
+        $this->assertTrue(MockSocket::isEmpty());
+
+        MockSocket::initialize('close-remote', $this);
+
+        $message = $client->receive();
+        $this->assertNull($message);
+
+        $this->assertFalse($client->isConnected());
+        $this->assertEquals(17260, $client->getCloseStatus());
+        $this->assertNull($client->getLastOpcode());
+        $this->assertTrue(MockSocket::isEmpty());
+    }
+
+    public function testSetTimeout(): void
+    {
+        MockSocket::initialize('client.connect', $this);
+        $client = new Client('ws://localhost:8000/my/mock/path');
+        $client->send('Connect');
+        $this->assertTrue(MockSocket::isEmpty());
+
+        MockSocket::initialize('config-timeout', $this);
+        $client->setTimeout(300);
+        $this->assertTrue($client->isConnected());
+        $this->assertTrue(MockSocket::isEmpty());
+    }
+
+    public function testReconnect(): void
+    {
+        MockSocket::initialize('client.connect', $this);
+        $client = new Client('ws://localhost:8000/my/mock/path');
+        $client->send('Connect');
+        $this->assertTrue(MockSocket::isEmpty());
+
+        MockSocket::initialize('client.close', $this);
+        $this->assertTrue($client->isConnected());
+        $this->assertNull($client->getCloseStatus());
+        $client->close();
+        $this->assertFalse($client->isConnected());
+        $this->assertEquals(1000, $client->getCloseStatus());
+        $this->assertNull($client->getLastOpcode());
+        $this->assertTrue(MockSocket::isEmpty());
+
+        MockSocket::initialize('client.reconnect', $this);
+        $message = $client->receive();
+        $this->assertTrue($client->isConnected());
+        $this->assertTrue(MockSocket::isEmpty());
+    }
+
+    public function testPersistentConnection(): void
+    {
+        MockSocket::initialize('client.connect-persistent', $this);
+        $client = new Client('ws://localhost:8000/my/mock/path', ['persistent' => true]);
+        $client->send('Connect');
+        $client->disconnect();
+        $this->assertFalse($client->isConnected());
+        $this->assertTrue(MockSocket::isEmpty());
+    }
+
+    public function testFailedPersistentConnection(): void
+    {
+        MockSocket::initialize('client.connect-persistent-failure', $this);
+        $client = new Client('ws://localhost:8000/my/mock/path', ['persistent' => true]);
+        $this->expectException('WebSocket\ConnectionException');
+        $this->expectExceptionMessage('Could not resolve stream pointer position');
+        $client->send('Connect');
+    }
+
+    public function testBadScheme(): void
+    {
+        MockSocket::initialize('client.connect', $this);
+        $this->expectException('WebSocket\BadUriException');
+        $this->expectExceptionMessage("Invalid URI scheme, must be 'ws' or 'wss'.");
+        $client = new Client('bad://localhost:8000/my/mock/path');
+    }
+
+    public function testBadUri(): void
+    {
+        MockSocket::initialize('client.connect', $this);
+        $this->expectException('WebSocket\BadUriException');
+        $this->expectExceptionMessage("Invalid URI '--:this is not an uri:--' provided.");
+        $client = new Client('--:this is not an uri:--');
+    }
+
+    public function testInvalidUriType(): void
+    {
+        MockSocket::initialize('client.connect', $this);
+        $this->expectException('WebSocket\BadUriException');
+        $this->expectExceptionMessage("Provided URI must be a UriInterface or string.");
+        $client = new Client([]);
+    }
+
+    public function testUriInterface(): void
+    {
+        MockSocket::initialize('client.connect', $this);
+        $uri = new Uri('ws://localhost:8000/my/mock/path');
+        $client = new Client($uri);
+        $client->send('Connect');
+        $this->assertTrue(MockSocket::isEmpty());
+    }
+
+    public function testBadStreamContext(): void
+    {
+        MockSocket::initialize('client.connect-bad-context', $this);
+        $client = new Client('ws://localhost:8000/my/mock/path', ['context' => 'BAD']);
+        $this->expectException('InvalidArgumentException');
+        $this->expectExceptionMessage('Stream context in $options[\'context\'] isn\'t a valid context');
+        $client->send('Connect');
+    }
+
+    public function testFailedConnection(): void
+    {
+        MockSocket::initialize('client.connect-failed', $this);
+        $client = new Client('ws://localhost:8000/my/mock/path');
+        $this->expectException('WebSocket\ConnectionException');
+        $this->expectExceptionCode(0);
+        $this->expectExceptionMessage('Could not open socket to "localhost:8000"');
+        $client->send('Connect');
+    }
+
+    public function testFailedConnectionWithError(): void
+    {
+        MockSocket::initialize('client.connect-error', $this);
+        $client = new Client('ws://localhost:8000/my/mock/path');
+        $this->expectException('WebSocket\ConnectionException');
+        $this->expectExceptionCode(0);
+        $this->expectExceptionMessage('Could not open socket to "localhost:8000"');
+        $client->send('Connect');
+    }
+
+    public function testBadStreamConnection(): void
+    {
+        MockSocket::initialize('client.connect-bad-stream', $this);
+        $client = new Client('ws://localhost:8000/my/mock/path');
+        $this->expectException('WebSocket\ConnectionException');
+        $this->expectExceptionCode(0);
+        $this->expectExceptionMessage('Invalid stream on "localhost:8000"');
+        $client->send('Connect');
+    }
+
+    public function testHandshakeFailure(): void
+    {
+        MockSocket::initialize('client.connect-handshake-failure', $this);
+        $client = new Client('ws://localhost:8000/my/mock/path');
+        $this->expectException('WebSocket\ConnectionException');
+        $this->expectExceptionCode(0);
+        $this->expectExceptionMessage('Client handshake error');
+        $client->send('Connect');
+    }
+
+    public function testInvalidUpgrade(): void
+    {
+        MockSocket::initialize('client.connect-invalid-upgrade', $this);
+        $client = new Client('ws://localhost:8000/my/mock/path');
+        $this->expectException('WebSocket\ConnectionException');
+        $this->expectExceptionCode(0);
+        $this->expectExceptionMessage('Connection to \'ws://localhost:8000/my/mock/path\' failed');
+        $client->send('Connect');
+    }
+
+    public function testInvalidKey(): void
+    {
+        MockSocket::initialize('client.connect-invalid-key', $this);
+        $client = new Client('ws://localhost:8000/my/mock/path');
+        $this->expectException('WebSocket\ConnectionException');
+        $this->expectExceptionCode(0);
+        $this->expectExceptionMessage('Server sent bad upgrade response');
+        $client->send('Connect');
+    }
+
+    public function testSendBadOpcode(): void
+    {
+        MockSocket::initialize('client.connect', $this);
+        $client = new Client('ws://localhost:8000/my/mock/path');
+        $client->send('Connect');
+
+        MockSocket::initialize('send-bad-opcode', $this);
+        $this->expectException('WebSocket\BadOpcodeException');
+        $this->expectExceptionMessage('Bad opcode \'bad\'.  Try \'text\' or \'binary\'.');
+        $client->send('Bad Opcode', 'bad');
+    }
+
+    public function testRecieveBadOpcode(): void
+    {
+        MockSocket::initialize('client.connect', $this);
+        $client = new Client('ws://localhost:8000/my/mock/path');
+        $client->send('Connect');
+        MockSocket::initialize('receive-bad-opcode', $this);
+        $this->expectException('WebSocket\ConnectionException');
+        $this->expectExceptionCode(1026);
+        $this->expectExceptionMessage('Bad opcode in websocket frame: 12');
+        $message = $client->receive();
+    }
+
+    public function testBrokenWrite(): void
+    {
+        MockSocket::initialize('client.connect', $this);
+        $client = new Client('ws://localhost:8000/my/mock/path');
+        $client->send('Connect');
+        MockSocket::initialize('send-broken-write', $this);
+        $this->expectException('WebSocket\ConnectionException');
+        $this->expectExceptionCode(1025);
+        $this->expectExceptionMessage('Could only write 18 out of 22 bytes.');
+        $client->send('Failing to write');
+    }
+
+    public function testFailedWrite(): void
+    {
+        MockSocket::initialize('client.connect', $this);
+        $client = new Client('ws://localhost:8000/my/mock/path');
+        $client->send('Connect');
+        MockSocket::initialize('send-failed-write', $this);
+        $this->expectException('WebSocket\TimeoutException');
+        $this->expectExceptionCode(1024);
+        $this->expectExceptionMessage('Failed to write 22 bytes.');
+        $client->send('Failing to write');
+    }
+
+    public function testBrokenRead(): void
+    {
+        MockSocket::initialize('client.connect', $this);
+        $client = new Client('ws://localhost:8000/my/mock/path');
+        $client->send('Connect');
+        MockSocket::initialize('receive-broken-read', $this);
+        $this->expectException('WebSocket\ConnectionException');
+        $this->expectExceptionCode(1025);
+        $this->expectExceptionMessage('Broken frame, read 0 of stated 2 bytes.');
+        $client->receive();
+    }
+
+    public function testHandshakeError(): void
+    {
+        MockSocket::initialize('client.connect-handshake-error', $this);
+        $client = new Client('ws://localhost:8000/my/mock/path');
+        $this->expectException('WebSocket\ConnectionException');
+        $this->expectExceptionCode(1024);
+        $this->expectExceptionMessage('Client handshake error');
+        $client->send('Connect');
+    }
+
+    public function testReadTimeout(): void
+    {
+        MockSocket::initialize('client.connect', $this);
+        $client = new Client('ws://localhost:8000/my/mock/path');
+        $client->send('Connect');
+        MockSocket::initialize('receive-client-timeout', $this);
+        $this->expectException('WebSocket\TimeoutException');
+        $this->expectExceptionCode(1024);
+        $this->expectExceptionMessage('Client read timeout');
+        $client->receive();
+    }
+
+    public function testEmptyRead(): void
+    {
+        MockSocket::initialize('client.connect', $this);
+        $client = new Client('ws://localhost:8000/my/mock/path');
+        $client->send('Connect');
+        MockSocket::initialize('receive-empty-read', $this);
+        $this->expectException('WebSocket\TimeoutException');
+        $this->expectExceptionCode(1024);
+        $this->expectExceptionMessage('Empty read; connection dead?');
+        $client->receive();
+    }
+
+    public function testFrameFragmentation(): void
+    {
+        MockSocket::initialize('client.connect', $this);
+        $client = new Client(
+            'ws://localhost:8000/my/mock/path',
+            ['filter' => ['text', 'binary', 'pong', 'close']]
+        );
+        $client->send('Connect');
+        MockSocket::initialize('receive-fragmentation', $this);
+        $message = $client->receive();
+        $this->assertEquals('Server ping', $message);
+        $this->assertEquals('pong', $client->getLastOpcode());
+        $message = $client->receive();
+        $this->assertEquals('Multi fragment test', $message);
+        $this->assertEquals('text', $client->getLastOpcode());
+        $this->assertTrue(MockSocket::isEmpty());
+        MockSocket::initialize('close-remote', $this);
+        $message = $client->receive();
+        $this->assertEquals('Closing', $message);
+        $this->assertTrue(MockSocket::isEmpty());
+        $this->assertFalse($client->isConnected());
+        $this->assertEquals(17260, $client->getCloseStatus());
+        $this->assertEquals('close', $client->getLastOpcode());
+    }
+
+    public function testMessageFragmentation(): void
+    {
+        MockSocket::initialize('client.connect', $this);
+        $client = new Client(
+            'ws://localhost:8000/my/mock/path',
+            ['filter' => ['text', 'binary', 'pong', 'close'], 'return_obj' => true]
+        );
+        $client->send('Connect');
+        MockSocket::initialize('receive-fragmentation', $this);
+        $message = $client->receive();
+        $this->assertInstanceOf('WebSocket\Message\Message', $message);
+        $this->assertInstanceOf('WebSocket\Message\Pong', $message);
+        $this->assertEquals('Server ping', $message->getContent());
+        $this->assertEquals('pong', $message->getOpcode());
+        $message = $client->receive();
+        $this->assertInstanceOf('WebSocket\Message\Message', $message);
+        $this->assertInstanceOf('WebSocket\Message\Text', $message);
+        $this->assertEquals('Multi fragment test', $message->getContent());
+        $this->assertEquals('text', $message->getOpcode());
+        $this->assertTrue(MockSocket::isEmpty());
+        MockSocket::initialize('close-remote', $this);
+        $message = $client->receive();
+        $this->assertInstanceOf('WebSocket\Message\Message', $message);
+        $this->assertInstanceOf('WebSocket\Message\Close', $message);
+        $this->assertEquals('Closing', $message->getContent());
+        $this->assertEquals('close', $message->getOpcode());
+    }
+
+    public function testConvenicanceMethods(): void
+    {
+        MockSocket::initialize('client.connect', $this);
+        $client = new Client('ws://localhost:8000/my/mock/path');
+        $this->assertNull($client->getName());
+        $this->assertNull($client->getRemoteName());
+        $this->assertEquals('WebSocket\Client(closed)', "{$client}");
+        $client->text('Connect');
+        MockSocket::initialize('send-convenicance', $this);
+        $client->binary(base64_encode('Binary content'));
+        $client->ping();
+        $client->pong();
+        $this->assertEquals('127.0.0.1:12345', $client->getName());
+        $this->assertEquals('127.0.0.1:8000', $client->getRemoteName());
+        $this->assertEquals('WebSocket\Client(127.0.0.1:12345)', "{$client}");
+    }
+
+    public function testUnconnectedClient(): void
+    {
+        $client = new Client('ws://localhost:8000/my/mock/path');
+        $this->assertFalse($client->isConnected());
+        $client->setTimeout(30);
+        $client->close();
+        $this->assertFalse($client->isConnected());
+        $this->assertNull($client->getName());
+        $this->assertNull($client->getRemoteName());
+        $this->assertNull($client->getCloseStatus());
+    }
+
+    public function testDeprecated(): void
+    {
+        $client = new Client('ws://localhost:8000/my/mock/path');
+        (new ErrorHandler())->withAll(function () use ($client) {
+            $this->assertNull($client->getPier());
+        }, function ($exceptions, $result) {
+            $this->assertEquals(
+                'getPier() is deprecated and will be removed in future version. Use getRemoteName() instead.',
+                $exceptions[0]->getMessage()
+            );
+        }, E_USER_DEPRECATED);
+    }
+}

+ 51 - 0
vendor/textalk/websocket/tests/ExceptionTest.php

@@ -0,0 +1,51 @@
+<?php
+
+/**
+ * Test case for Exceptions.
+ */
+
+declare(strict_types=1);
+
+namespace WebSocket;
+
+use PHPUnit\Framework\TestCase;
+use Throwable;
+
+class ExceptionTest extends TestCase
+{
+    public function setUp(): void
+    {
+        error_reporting(-1);
+    }
+
+    public function testConnectionException(): void
+    {
+        try {
+            throw new ConnectionException(
+                'An error message',
+                ConnectionException::EOF,
+                ['test' => 'with data'],
+                new TimeoutException(
+                    'Nested exception',
+                    ConnectionException::TIMED_OUT
+                )
+            );
+        } catch (Throwable $e) {
+        }
+
+        $this->assertInstanceOf('WebSocket\ConnectionException', $e);
+        $this->assertInstanceOf('WebSocket\Exception', $e);
+        $this->assertInstanceOf('Exception', $e);
+        $this->assertInstanceOf('Throwable', $e);
+        $this->assertEquals('An error message', $e->getMessage());
+        $this->assertEquals(1025, $e->getCode());
+        $this->assertEquals(['test' => 'with data'], $e->getData());
+
+        $p = $e->getPrevious();
+        $this->assertInstanceOf('WebSocket\TimeoutException', $p);
+        $this->assertInstanceOf('WebSocket\ConnectionException', $p);
+        $this->assertEquals('Nested exception', $p->getMessage());
+        $this->assertEquals(1024, $p->getCode());
+        $this->assertEquals([], $p->getData());
+    }
+}

+ 60 - 0
vendor/textalk/websocket/tests/MessageTest.php

@@ -0,0 +1,60 @@
+<?php
+
+/**
+ * Test case for Message subsection.
+ */
+
+declare(strict_types=1);
+
+namespace WebSocket;
+
+use PHPUnit\Framework\TestCase;
+use WebSocket\Message\Factory;
+use WebSocket\Message\Text;
+
+class MessageTest extends TestCase
+{
+    public function setUp(): void
+    {
+        error_reporting(-1);
+    }
+
+    public function testFactory(): void
+    {
+        $factory = new Factory();
+        $message = $factory->create('text', 'Some content');
+        $this->assertInstanceOf('WebSocket\Message\Text', $message);
+        $message = $factory->create('binary', 'Some content');
+        $this->assertInstanceOf('WebSocket\Message\Binary', $message);
+        $message = $factory->create('ping', 'Some content');
+        $this->assertInstanceOf('WebSocket\Message\Ping', $message);
+        $message = $factory->create('pong', 'Some content');
+        $this->assertInstanceOf('WebSocket\Message\Pong', $message);
+        $message = $factory->create('close', 'Some content');
+        $this->assertInstanceOf('WebSocket\Message\Close', $message);
+    }
+
+    public function testMessage()
+    {
+        $message = new Text('Some content');
+        $this->assertInstanceOf('WebSocket\Message\Message', $message);
+        $this->assertInstanceOf('WebSocket\Message\Text', $message);
+        $this->assertEquals('Some content', $message->getContent());
+        $this->assertEquals('text', $message->getOpcode());
+        $this->assertEquals(12, $message->getLength());
+        $this->assertTrue($message->hasContent());
+        $this->assertInstanceOf('DateTime', $message->getTimestamp());
+        $message->setContent('');
+        $this->assertEquals(0, $message->getLength());
+        $this->assertFalse($message->hasContent());
+        $this->assertEquals('WebSocket\Message\Text', "{$message}");
+    }
+
+    public function testBadOpcode()
+    {
+        $factory = new Factory();
+        $this->expectException('WebSocket\BadOpcodeException');
+        $this->expectExceptionMessage("Invalid opcode 'invalid' provided");
+        $message = $factory->create('invalid', 'Some content');
+    }
+}

+ 28 - 0
vendor/textalk/websocket/tests/README.md

@@ -0,0 +1,28 @@
+# Testing
+
+Unit tests with [PHPUnit](https://phpunit.readthedocs.io/).
+
+
+## How to run
+
+To run all test, run in console.
+
+```
+make test
+```
+
+
+## Continuous integration
+
+GitHub Actions are run on PHP versions `7.4`, `8.0`, `8.1` and `8.2`.
+
+Code coverage by [Coveralls](https://coveralls.io/github/Textalk/websocket-php).
+
+
+## Test strategy
+
+Test set up overloads various stream and socket functions,
+and use "scripts" to define and mock input/output of these functions.
+
+This set up negates the dependency on running servers,
+and allow testing various errors that might occur.

+ 511 - 0
vendor/textalk/websocket/tests/ServerTest.php

@@ -0,0 +1,511 @@
+<?php
+
+/**
+ * Test case for Server.
+ * Note that this test is performed by mocking socket/stream calls.
+ */
+
+declare(strict_types=1);
+
+namespace WebSocket;
+
+use ErrorException;
+use Phrity\Util\ErrorHandler;
+use PHPUnit\Framework\TestCase;
+
+class ServerTest extends TestCase
+{
+    public function setUp(): void
+    {
+        error_reporting(-1);
+    }
+
+    public function testServerMasked(): void
+    {
+        MockSocket::initialize('server.construct', $this);
+        $server = new Server();
+        $this->assertTrue(MockSocket::isEmpty());
+        MockSocket::initialize('server.accept', $this);
+        $server->accept();
+        $server->send('Connect');
+        $this->assertEquals(8000, $server->getPort());
+        $this->assertEquals('/my/mock/path', $server->getPath());
+        $this->assertTrue($server->isConnected());
+        $this->assertEquals(4096, $server->getFragmentSize());
+        $this->assertNull($server->getCloseStatus());
+        $this->assertEquals([
+            'GET /my/mock/path HTTP/1.1',
+            'host: localhost:8000',
+            'user-agent: websocket-client-php',
+            'connection: Upgrade',
+            'upgrade: websocket',
+            'sec-websocket-key: cktLWXhUdDQ2OXF0ZCFqOQ==',
+            'sec-websocket-version: 13',
+            '',
+            '',
+        ], $server->getRequest());
+        $this->assertEquals('websocket-client-php', $server->getHeader('USER-AGENT'));
+        $this->assertNull($server->getHeader('no such header'));
+        $this->assertTrue(MockSocket::isEmpty());
+
+        MockSocket::initialize('send-receive', $this);
+        $server->send('Sending a message');
+        $message = $server->receive();
+        $this->assertEquals('Receiving a message', $message);
+        $this->assertTrue(MockSocket::isEmpty());
+        $this->assertNull($server->getCloseStatus());
+        $this->assertEquals('text', $server->getLastOpcode());
+
+        MockSocket::initialize('server.close', $this);
+        $server->close();
+        $this->assertFalse($server->isConnected());
+        $this->assertEquals(1000, $server->getCloseStatus());
+        $this->assertTrue(MockSocket::isEmpty());
+
+        $server->close(); // Already closed
+    }
+
+    public function testDestruct(): void
+    {
+        MockSocket::initialize('server.construct', $this);
+        $server = new Server();
+
+        MockSocket::initialize('server.accept-destruct', $this);
+        $server->accept();
+        $message = $server->receive();
+    }
+
+    public function testServerWithTimeout(): void
+    {
+        MockSocket::initialize('server.construct', $this);
+        $server = new Server(['timeout' => 300]);
+        $this->assertTrue(MockSocket::isEmpty());
+
+        MockSocket::initialize('server.accept-timeout', $this);
+        $server->accept();
+        $server->send('Connect');
+        $this->assertTrue(MockSocket::isEmpty());
+    }
+
+    public function testPayload128(): void
+    {
+        MockSocket::initialize('server.construct', $this);
+        $server = new Server();
+        $this->assertTrue(MockSocket::isEmpty());
+
+        MockSocket::initialize('server.accept', $this);
+        $server->accept();
+        $server->send('Connect');
+        $this->assertTrue($server->isConnected());
+        $this->assertTrue(MockSocket::isEmpty());
+
+        $payload = file_get_contents(__DIR__ . '/mock/payload.128.txt');
+
+        MockSocket::initialize('send-receive-128', $this);
+        $server->send($payload, 'text', false);
+        $message = $server->receive();
+        $this->assertEquals($payload, $message);
+        $this->assertTrue(MockSocket::isEmpty());
+    }
+
+    public function testPayload65536(): void
+    {
+        MockSocket::initialize('server.construct', $this);
+        $server = new Server();
+        $this->assertTrue(MockSocket::isEmpty());
+
+        MockSocket::initialize('server.accept', $this);
+        $server->accept();
+        $server->send('Connect');
+        $this->assertTrue($server->isConnected());
+        $this->assertTrue(MockSocket::isEmpty());
+
+        $payload = file_get_contents(__DIR__ . '/mock/payload.65536.txt');
+        $server->setFragmentSize(65540);
+
+        MockSocket::initialize('send-receive-65536', $this);
+        $server->send($payload, 'text', false);
+        $message = $server->receive();
+        $this->assertEquals($payload, $message);
+        $this->assertTrue(MockSocket::isEmpty());
+    }
+
+    public function testMultiFragment(): void
+    {
+        MockSocket::initialize('server.construct', $this);
+        $server = new Server();
+        $this->assertTrue(MockSocket::isEmpty());
+
+        MockSocket::initialize('server.accept', $this);
+        $server->accept();
+        $server->send('Connect');
+        $this->assertTrue($server->isConnected());
+        $this->assertTrue(MockSocket::isEmpty());
+
+        MockSocket::initialize('send-receive-multi-fragment', $this);
+        $server->setFragmentSize(8);
+        $server->send('Multi fragment test');
+        $message = $server->receive();
+        $this->assertEquals('Multi fragment test', $message);
+        $this->assertTrue(MockSocket::isEmpty());
+    }
+
+    public function testPingPong(): void
+    {
+        MockSocket::initialize('server.construct', $this);
+        $server = new Server();
+        $this->assertTrue(MockSocket::isEmpty());
+
+        MockSocket::initialize('server.accept', $this);
+        $server->accept();
+        $server->send('Connect');
+        $this->assertTrue($server->isConnected());
+        $this->assertTrue(MockSocket::isEmpty());
+
+        MockSocket::initialize('ping-pong', $this);
+        $server->send('Server ping', 'ping');
+        $server->send('', 'ping');
+        $message = $server->receive();
+        $this->assertEquals('Receiving a message', $message);
+        $this->assertEquals('text', $server->getLastOpcode());
+        $this->assertTrue(MockSocket::isEmpty());
+    }
+
+    public function testRemoteClose(): void
+    {
+        MockSocket::initialize('server.construct', $this);
+        $server = new Server();
+        $this->assertTrue(MockSocket::isEmpty());
+
+        MockSocket::initialize('server.accept', $this);
+        $server->accept();
+        $server->send('Connect');
+        $this->assertTrue($server->isConnected());
+        $this->assertTrue(MockSocket::isEmpty());
+
+        MockSocket::initialize('close-remote', $this);
+
+        $message = $server->receive();
+        $this->assertEquals('', $message);
+
+        $this->assertTrue(MockSocket::isEmpty());
+        $this->assertFalse($server->isConnected());
+        $this->assertEquals(17260, $server->getCloseStatus());
+        $this->assertNull($server->getLastOpcode());
+    }
+
+    public function testSetTimeout(): void
+    {
+        MockSocket::initialize('server.construct', $this);
+        $server = new Server();
+        $this->assertTrue(MockSocket::isEmpty());
+
+        MockSocket::initialize('server.accept', $this);
+        $server->accept();
+        $server->send('Connect');
+        $this->assertTrue($server->isConnected());
+        $this->assertTrue(MockSocket::isEmpty());
+
+        MockSocket::initialize('config-timeout', $this);
+        $server->setTimeout(300);
+        $this->assertTrue($server->isConnected());
+        $this->assertTrue(MockSocket::isEmpty());
+    }
+
+    public function testFailedSocketServer(): void
+    {
+        MockSocket::initialize('server.construct-failed-socket-server', $this);
+        $this->expectException('WebSocket\ConnectionException');
+        $this->expectExceptionCode(0);
+        $this->expectExceptionMessage('Could not open listening socket:');
+        $server = new Server(['port' => 9999]);
+    }
+
+    public function testFailedSocketServerWithError(): void
+    {
+        MockSocket::initialize('server.construct-error-socket-server', $this);
+        $this->expectException('WebSocket\ConnectionException');
+        $this->expectExceptionCode(0);
+        $this->expectExceptionMessage('Could not open listening socket:');
+        $server = new Server(['port' => 9999]);
+    }
+
+    public function testFailedConnect(): void
+    {
+        MockSocket::initialize('server.construct', $this);
+        $server = new Server();
+
+        MockSocket::initialize('server.accept-failed-connect', $this);
+        $server->accept();
+        $this->expectException('WebSocket\ConnectionException');
+        $this->expectExceptionCode(0);
+        $this->expectExceptionMessage('Server failed to connect');
+        $server->send('Connect');
+    }
+
+    public function testFailedConnectWithError(): void
+    {
+        MockSocket::initialize('server.construct', $this);
+        $server = new Server();
+
+        MockSocket::initialize('server.accept-error-connect', $this);
+        $server->accept();
+        $this->expectException('WebSocket\ConnectionException');
+        $this->expectExceptionCode(0);
+        $this->expectExceptionMessage('Server failed to connect');
+        $server->send('Connect');
+    }
+
+    public function testFailedConnectTimeout(): void
+    {
+        MockSocket::initialize('server.construct', $this);
+        $server = new Server(['timeout' => 300]);
+
+        MockSocket::initialize('server.accept-failed-connect', $this);
+        $server->accept();
+        $this->expectException('WebSocket\ConnectionException');
+        $this->expectExceptionCode(0);
+        $this->expectExceptionMessage('Server failed to connect');
+        $server->send('Connect');
+    }
+
+    public function testFailedHttp(): void
+    {
+        MockSocket::initialize('server.construct', $this);
+        $server = new Server();
+        MockSocket::initialize('server.accept-failed-http', $this);
+        $server->accept();
+        $this->expectException('WebSocket\ConnectionException');
+        $this->expectExceptionCode(0);
+        $this->expectExceptionMessage('No GET in request');
+        $server->send('Connect');
+    }
+
+    public function testFailedWsKey(): void
+    {
+        MockSocket::initialize('server.construct', $this);
+        $server = new Server();
+        MockSocket::initialize('server.accept-failed-ws-key', $this);
+        $server->accept();
+        $this->expectException('WebSocket\ConnectionException');
+        $this->expectExceptionCode(0);
+        $this->expectExceptionMessage('Client had no Key in upgrade request');
+        $server->send('Connect');
+    }
+
+    public function testSendBadOpcode(): void
+    {
+        MockSocket::initialize('server.construct', $this);
+        $server = new Server();
+        MockSocket::initialize('server.accept', $this);
+        $server->accept();
+        $server->send('Connect');
+        $this->expectException('WebSocket\BadOpcodeException');
+        $this->expectExceptionCode(0);
+        $this->expectExceptionMessage('Bad opcode \'bad\'.  Try \'text\' or \'binary\'.');
+        $server->send('Bad Opcode', 'bad');
+    }
+
+    public function testRecieveBadOpcode(): void
+    {
+        MockSocket::initialize('server.construct', $this);
+        $server = new Server();
+        MockSocket::initialize('server.accept', $this);
+        $server->accept();
+        $server->send('Connect');
+        MockSocket::initialize('receive-bad-opcode', $this);
+        $this->expectException('WebSocket\ConnectionException');
+        $this->expectExceptionCode(1026);
+        $this->expectExceptionMessage('Bad opcode in websocket frame: 12');
+        $message = $server->receive();
+    }
+
+    public function testBrokenWrite(): void
+    {
+        MockSocket::initialize('server.construct', $this);
+        $server = new Server();
+        MockSocket::initialize('server.accept', $this);
+        $server->accept();
+        $server->send('Connect');
+        MockSocket::initialize('send-broken-write', $this);
+        $this->expectException('WebSocket\ConnectionException');
+        $this->expectExceptionCode(1025);
+        $this->expectExceptionMessage('Could only write 18 out of 22 bytes.');
+        $server->send('Failing to write');
+    }
+
+    public function testFailedWrite(): void
+    {
+        MockSocket::initialize('server.construct', $this);
+        $server = new Server();
+        MockSocket::initialize('server.accept', $this);
+        $server->accept();
+        $server->send('Connect');
+        MockSocket::initialize('send-failed-write', $this);
+        $this->expectException('WebSocket\TimeoutException');
+        $this->expectExceptionCode(1024);
+        $this->expectExceptionMessage('Failed to write 22 bytes.');
+        $server->send('Failing to write');
+    }
+
+    public function testBrokenRead(): void
+    {
+        MockSocket::initialize('server.construct', $this);
+        $server = new Server();
+        MockSocket::initialize('server.accept', $this);
+        $server->accept();
+        $server->send('Connect');
+        MockSocket::initialize('receive-broken-read', $this);
+        $this->expectException('WebSocket\ConnectionException');
+        $this->expectExceptionCode(1025);
+        $this->expectExceptionMessage('Broken frame, read 0 of stated 2 bytes.');
+        $server->receive();
+    }
+
+    public function testEmptyRead(): void
+    {
+        MockSocket::initialize('server.construct', $this);
+        $server = new Server();
+        MockSocket::initialize('server.accept', $this);
+        $server->accept();
+        $server->send('Connect');
+        MockSocket::initialize('receive-empty-read', $this);
+        $this->expectException('WebSocket\TimeoutException');
+        $this->expectExceptionCode(1024);
+        $this->expectExceptionMessage('Empty read; connection dead?');
+        $server->receive();
+    }
+
+    public function testFrameFragmentation(): void
+    {
+        MockSocket::initialize('server.construct', $this);
+        $server = new Server(['filter' => ['text', 'binary', 'pong', 'close']]);
+        MockSocket::initialize('server.accept', $this);
+        $server->accept();
+        $server->send('Connect');
+        MockSocket::initialize('receive-fragmentation', $this);
+        $message = $server->receive();
+        $this->assertEquals('Server ping', $message);
+        $this->assertEquals('pong', $server->getLastOpcode());
+        $message = $server->receive();
+        $this->assertEquals('Multi fragment test', $message);
+        $this->assertEquals('text', $server->getLastOpcode());
+        $this->assertTrue(MockSocket::isEmpty());
+        MockSocket::initialize('close-remote', $this);
+        $message = $server->receive();
+        $this->assertEquals('Closing', $message);
+        $this->assertTrue(MockSocket::isEmpty());
+        $this->assertFalse($server->isConnected());
+        $this->assertEquals(17260, $server->getCloseStatus());
+        $this->assertEquals('close', $server->getLastOpcode());
+    }
+
+    public function testMessageFragmentation(): void
+    {
+        MockSocket::initialize('server.construct', $this);
+        $server = new Server(['filter' => ['text', 'binary', 'pong', 'close'], 'return_obj' => true]);
+        MockSocket::initialize('server.accept', $this);
+        $server->accept();
+        $server->send('Connect');
+        MockSocket::initialize('receive-fragmentation', $this);
+        $message = $server->receive();
+        $this->assertInstanceOf('WebSocket\Message\Message', $message);
+        $this->assertInstanceOf('WebSocket\Message\Pong', $message);
+        $this->assertEquals('Server ping', $message->getContent());
+        $this->assertEquals('pong', $message->getOpcode());
+        $message = $server->receive();
+        $this->assertInstanceOf('WebSocket\Message\Message', $message);
+        $this->assertInstanceOf('WebSocket\Message\Text', $message);
+        $this->assertEquals('Multi fragment test', $message->getContent());
+        $this->assertEquals('text', $message->getOpcode());
+        $this->assertTrue(MockSocket::isEmpty());
+        MockSocket::initialize('close-remote', $this);
+        $message = $server->receive();
+        $this->assertInstanceOf('WebSocket\Message\Message', $message);
+        $this->assertInstanceOf('WebSocket\Message\Close', $message);
+        $this->assertEquals('Closing', $message->getContent());
+        $this->assertEquals('close', $message->getOpcode());
+    }
+
+    public function testConvenicanceMethods(): void
+    {
+        MockSocket::initialize('server.construct', $this);
+        $server = new Server();
+        $this->assertNull($server->getName());
+        $this->assertNull($server->getRemoteName());
+        $this->assertEquals('WebSocket\Server(closed)', "{$server}");
+        MockSocket::initialize('server.accept', $this);
+        $server->accept();
+        $server->text('Connect');
+        MockSocket::initialize('send-convenicance', $this);
+        $server->binary(base64_encode('Binary content'));
+        $server->ping();
+        $server->pong();
+        $this->assertEquals('127.0.0.1:12345', $server->getName());
+        $this->assertEquals('127.0.0.1:8000', $server->getRemoteName());
+        $this->assertEquals('WebSocket\Server(127.0.0.1:12345)', "{$server}");
+        $this->assertTrue(MockSocket::isEmpty());
+    }
+
+    public function testUnconnectedServer(): void
+    {
+        MockSocket::initialize('server.construct', $this);
+        $server = new Server();
+        $this->assertFalse($server->isConnected());
+        $server->setTimeout(30);
+        $server->close();
+        $this->assertFalse($server->isConnected());
+        $this->assertNull($server->getName());
+        $this->assertNull($server->getRemoteName());
+        $this->assertNull($server->getCloseStatus());
+        $this->assertTrue(MockSocket::isEmpty());
+    }
+
+    public function testFailedHandshake(): void
+    {
+        MockSocket::initialize('server.construct', $this);
+        $server = new Server();
+        $this->assertTrue(MockSocket::isEmpty());
+
+        MockSocket::initialize('server.accept-failed-handshake', $this);
+        $server->accept();
+        $this->expectException('WebSocket\ConnectionException');
+        $this->expectExceptionCode(0);
+        $this->expectExceptionMessage('Could not read from stream');
+        $server->send('Connect');
+        $this->assertFalse($server->isConnected());
+        $this->assertTrue(MockSocket::isEmpty());
+    }
+
+    public function testServerDisconnect(): void
+    {
+        MockSocket::initialize('server.construct', $this);
+        $server = new Server();
+        $this->assertTrue(MockSocket::isEmpty());
+        MockSocket::initialize('server.accept', $this);
+        $server->accept();
+        $server->send('Connect');
+        $this->assertTrue($server->isConnected());
+        $this->assertTrue(MockSocket::isEmpty());
+
+        MockSocket::initialize('server.disconnect', $this);
+        $server->disconnect();
+        $this->assertFalse($server->isConnected());
+        $this->assertTrue(MockSocket::isEmpty());
+    }
+
+    public function testDeprecated(): void
+    {
+        MockSocket::initialize('server.construct', $this);
+        $server = new Server();
+        $this->assertTrue(MockSocket::isEmpty());
+        (new ErrorHandler())->withAll(function () use ($server) {
+            $this->assertNull($server->getPier());
+        }, function ($exceptions, $result) {
+            $this->assertEquals(
+                'getPier() is deprecated and will be removed in future version. Use getRemoteName() instead.',
+                $exceptions[0]->getMessage()
+            );
+        }, E_USER_DEPRECATED);
+    }
+}

+ 6 - 0
vendor/textalk/websocket/tests/bootstrap.php

@@ -0,0 +1,6 @@
+<?php
+
+namespace WebSocket;
+
+require dirname(__DIR__) . '/vendor/autoload.php';
+require __DIR__ . '/mock/mock-socket.php';

Some files were not shown because too many files changed in this diff