From 93a321ff5b77e698e52d0e204487a968df4b5fc9 Mon Sep 17 00:00:00 2001 From: Jay Collett <486430+jaycollett@users.noreply.github.com> Date: Thu, 29 Jan 2026 14:38:27 -0500 Subject: [PATCH 1/2] Add WebSocket support for TrueNAS JSON-RPC 2.0 API TrueNAS is deprecating the REST API (api/v2.0/) in version 26.04, requiring migration to JSON-RPC 2.0 over WebSocket. This commit adds: - phrity/websocket dependency for WebSocket communication - TrueNASWebSocketClient helper class that handles: - Connection to ws(s)://host/api/current - Authentication via auth.login_with_api_key - JSON-RPC 2.0 request/response formatting - TLS verification toggle - Proper connection cleanup Refs: #1530 --- app/Helpers/TrueNASWebSocketClient.php | 187 +++++++++++++++++++++++++ composer.json | 1 + 2 files changed, 188 insertions(+) create mode 100644 app/Helpers/TrueNASWebSocketClient.php diff --git a/app/Helpers/TrueNASWebSocketClient.php b/app/Helpers/TrueNASWebSocketClient.php new file mode 100644 index 000000000..17c32df08 --- /dev/null +++ b/app/Helpers/TrueNASWebSocketClient.php @@ -0,0 +1,187 @@ +url = "{$wsScheme}://{$host}{$portPart}/api/current"; + $this->apiKey = $apiKey; + $this->ignoreTls = $ignoreTls; + } + + /** + * Connect to the TrueNAS WebSocket API and authenticate. + * + * @return bool True if connection and authentication succeeded + * @throws \Exception If connection or authentication fails + */ + public function connect(): bool + { + if ($this->client !== null && $this->authenticated) { + return true; + } + + // Build SSL options - always force HTTP/1.1 via ALPN for WebSocket compatibility + // TrueNAS nginx defaults to HTTP/2 which doesn't support WebSocket upgrade + $sslOptions = [ + 'alpn_protocols' => 'http/1.1', + ]; + + if ($this->ignoreTls) { + $sslOptions['verify_peer'] = false; + $sslOptions['verify_peer_name'] = false; + $sslOptions['allow_self_signed'] = true; + } + + // Create context using phrity/net-stream Context class (required by phrity/websocket v3.x) + $streamContext = stream_context_create(['ssl' => $sslOptions]); + $context = new Context($streamContext); + + try { + $this->client = new Client($this->url); + $this->client->setTimeout(15); + $this->client->setContext($context); + + $authResult = $this->call('auth.login_with_api_key', [$this->apiKey]); + + if ($authResult === true) { + $this->authenticated = true; + return true; + } + + throw new \Exception('Authentication failed: Invalid API key'); + } catch (ConnectionException $e) { + Log::error('TrueNAS WebSocket connection failed: ' . $e->getMessage()); + $this->disconnect(); + throw new \Exception('WebSocket connection failed: ' . $e->getMessage()); + } + } + + /** + * Make a JSON-RPC 2.0 call to the TrueNAS API. + * + * @param string $method The JSON-RPC method name (e.g., 'system.info') + * @param array $params Optional parameters for the method + * @return mixed The result from the API call + * @throws \Exception If the call fails or returns an error + */ + public function call(string $method, array $params = []) + { + if ($this->client === null) { + throw new \Exception('WebSocket client not connected'); + } + + $request = [ + 'jsonrpc' => '2.0', + 'method' => $method, + 'id' => $this->requestId++, + ]; + + if (!empty($params)) { + $request['params'] = $params; + } + + try { + $this->client->text(json_encode($request)); + $response = $this->client->receive(); + $decoded = json_decode($response->getContent(), true); + + if (isset($decoded['error'])) { + $errorMsg = $decoded['error']['message'] ?? 'Unknown error'; + $errorCode = $decoded['error']['code'] ?? 0; + throw new \Exception("API error ({$errorCode}): {$errorMsg}"); + } + + return $decoded['result'] ?? null; + } catch (ConnectionException $e) { + Log::error('TrueNAS WebSocket call failed: ' . $e->getMessage()); + throw new \Exception('WebSocket call failed: ' . $e->getMessage()); + } + } + + /** + * Close the WebSocket connection. + */ + public function disconnect(): void + { + if ($this->client !== null) { + try { + $this->client->close(); + } catch (\Exception $e) { + Log::debug('Error closing WebSocket: ' . $e->getMessage()); + } + $this->client = null; + $this->authenticated = false; + } + } + + /** + * Check if the client is connected and authenticated. + * + * @return bool + */ + public function isConnected(): bool + { + return $this->client !== null && $this->authenticated; + } + + /** + * Test the connection by calling core.ping. + * + * @return bool True if the ping succeeds + */ + public function ping(): bool + { + try { + $result = $this->call('core.ping'); + return $result === 'pong'; + } catch (\Exception $e) { + return false; + } + } + + /** + * Clean up on destruction. + */ + public function __destruct() + { + $this->disconnect(); + } +} diff --git a/composer.json b/composer.json index fc81bdfc7..33b475fcc 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,7 @@ "laravel/ui": "^4.4", "league/flysystem-aws-s3-v3": "^3.0", "nunomaduro/collision": "^8.0", + "phrity/websocket": "^3.6", "spatie/laravel-html": "^3.11", "spatie/laravel-ignition": "^2.4", "symfony/yaml": "^7.0" From d16b7f60f3551f05f6e0341ba20f9f4151b6eabb Mon Sep 17 00:00:00 2001 From: KodeStar Date: Thu, 9 Jul 2026 22:01:18 +0100 Subject: [PATCH 2/2] Vendor phrity/websocket dependency and fix v3 exception namespace The WebSocket support added for TrueNAS JSON-RPC 2.0 requires the phrity/websocket library to actually be available at runtime. This repo commits the vendor/ tree (CI does not run composer install), so the dependency and composer.lock must be committed for class_exists() checks in the TrueNAS app to succeed. - composer require phrity/websocket:^3.6 (resolves to 3.7.3) with lock and vendor/ committed - Fix TrueNASWebSocketClient to catch WebSocket\Exception\Exception (phrity/websocket v3 namespace) instead of the non-existent WebSocket\ConnectionException from the old textalk/websocket v1/v2 API, so connection/call failures are logged and wrapped as intended --- app/Helpers/TrueNASWebSocketClient.php | 6 +- composer.lock | 600 +++++++++++++- vendor/composer/autoload_classmap.php | 127 +++ vendor/composer/autoload_psr4.php | 8 + vendor/composer/autoload_static.php | 169 ++++ vendor/composer/installed.json | 628 ++++++++++++++ vendor/composer/installed.php | 94 ++- vendor/nyholm/psr7/CHANGELOG.md | 172 ++++ vendor/nyholm/psr7/LICENSE | 21 + vendor/nyholm/psr7/README.md | 108 +++ vendor/nyholm/psr7/composer.json | 49 ++ .../psr7/src/Factory/HttplugFactory.php | 53 ++ .../nyholm/psr7/src/Factory/Psr17Factory.php | 78 ++ vendor/nyholm/psr7/src/MessageTrait.php | 235 ++++++ vendor/nyholm/psr7/src/Request.php | 47 ++ vendor/nyholm/psr7/src/RequestTrait.php | 127 +++ vendor/nyholm/psr7/src/Response.php | 93 +++ vendor/nyholm/psr7/src/ServerRequest.php | 201 +++++ vendor/nyholm/psr7/src/Stream.php | 399 +++++++++ vendor/nyholm/psr7/src/StreamTrait.php | 57 ++ vendor/nyholm/psr7/src/UploadedFile.php | 179 ++++ vendor/nyholm/psr7/src/Uri.php | 356 ++++++++ vendor/phrity/comparison/composer.json | 34 + vendor/phrity/comparison/src/Comparable.php | 56 ++ vendor/phrity/comparison/src/Comparator.php | 200 +++++ .../phrity/comparison/src/ComparisonTrait.php | 69 ++ vendor/phrity/comparison/src/Equalable.php | 22 + .../comparison/src/IncomparableException.php | 17 + vendor/phrity/http/LICENSE | 21 + vendor/phrity/http/composer.json | 40 + vendor/phrity/http/src/HttpFactory.php | 175 ++++ vendor/phrity/http/src/Serializer.php | 77 ++ vendor/phrity/net-stream/LICENSE | 21 + vendor/phrity/net-stream/composer.json | 39 + vendor/phrity/net-stream/src/Context.php | 214 +++++ vendor/phrity/net-stream/src/SocketClient.php | 108 +++ vendor/phrity/net-stream/src/SocketServer.php | 173 ++++ vendor/phrity/net-stream/src/SocketStream.php | 166 ++++ vendor/phrity/net-stream/src/Stream.php | 316 ++++++++ .../net-stream/src/StreamCollection.php | 220 +++++ .../src/StreamContainerInterface.php | 14 + .../phrity/net-stream/src/StreamException.php | 85 ++ .../phrity/net-stream/src/StreamFactory.php | 133 +++ .../phrity/net-stream/src/StreamInterface.php | 21 + vendor/phrity/net-uri/LICENSE | 21 + vendor/phrity/net-uri/composer.json | 37 + vendor/phrity/net-uri/src/Uri.php | 710 ++++++++++++++++ vendor/phrity/net-uri/src/UriFactory.php | 47 ++ vendor/phrity/util-accessor/LICENSE | 21 + vendor/phrity/util-accessor/composer.json | 35 + vendor/phrity/util-accessor/src/Accessor.php | 65 ++ .../util-accessor/src/AccessorException.php | 12 + .../util-accessor/src/AccessorTrait.php | 124 +++ .../phrity/util-accessor/src/DataAccessor.php | 76 ++ .../phrity/util-accessor/src/PathAccessor.php | 68 ++ vendor/phrity/util-errorhandler/LICENSE | 21 + vendor/phrity/util-errorhandler/composer.json | 34 + .../util-errorhandler/src/ErrorHandler.php | 131 +++ vendor/phrity/util-interpolator/LICENSE | 21 + vendor/phrity/util-interpolator/composer.json | 36 + .../util-interpolator/src/Interpolator.php | 34 + .../src/InterpolatorTrait.php | 47 ++ vendor/phrity/util-transformer/LICENSE | 21 + vendor/phrity/util-transformer/composer.json | 42 + .../src/BasicTypeConverter.php | 70 ++ .../util-transformer/src/ChainedResolver.php | 56 ++ .../src/DateTimeConverter.php | 50 ++ .../util-transformer/src/EnumConverter.php | 30 + .../src/FirstMatchResolver.php | 54 ++ .../util-transformer/src/FlattenDecoder.php | 47 ++ .../util-transformer/src/JsonDecoder.php | 44 + .../src/JsonSerializableConverter.php | 25 + .../src/ReadableConverter.php | 32 + .../src/RecursionResolver.php | 38 + .../src/ReversedReadableConverter.php | 33 + .../util-transformer/src/StringResolver.php | 77 ++ .../src/StringableConverter.php | 30 + .../src/Symfony/NormalizerWrapper.php | 36 + .../src/ThrowableConverter.php | 77 ++ .../src/TransformerException.php | 12 + .../src/TransformerInterface.php | 9 + vendor/phrity/util-transformer/src/Type.php | 14 + vendor/phrity/websocket/LICENSE.md | 16 + vendor/phrity/websocket/composer.json | 50 ++ vendor/phrity/websocket/src/Client.php | 764 ++++++++++++++++++ vendor/phrity/websocket/src/Configuration.php | 195 +++++ vendor/phrity/websocket/src/Connection.php | 498 ++++++++++++ vendor/phrity/websocket/src/Constant.php | 17 + .../src/Exception/BadOpcodeException.php | 20 + .../src/Exception/BadUriException.php | 20 + .../src/Exception/ClientException.php | 16 + .../src/Exception/CloseException.php | 29 + .../Exception/ConnectionClosedException.php | 20 + .../Exception/ConnectionFailureException.php | 20 + .../Exception/ConnectionLevelInterface.php | 16 + .../Exception/ConnectionTimeoutException.php | 20 + .../websocket/src/Exception/Exception.php | 18 + .../src/Exception/ExceptionInterface.php | 19 + .../src/Exception/HandshakeException.php | 30 + .../src/Exception/MessageLevelInterface.php | 16 + .../src/Exception/ReconnectException.php | 30 + .../src/Exception/RunnerException.php | 16 + .../src/Exception/ServerException.php | 16 + vendor/phrity/websocket/src/Frame/Frame.php | 93 +++ .../websocket/src/Frame/FrameHandler.php | 236 ++++++ .../websocket/src/Http/DefaultHttpFactory.php | 72 ++ .../phrity/websocket/src/Http/HttpHandler.php | 143 ++++ vendor/phrity/websocket/src/Http/Request.php | 28 + vendor/phrity/websocket/src/Http/Response.php | 25 + .../websocket/src/Http/ServerRequest.php | 28 + .../phrity/websocket/src/Message/Binary.php | 27 + vendor/phrity/websocket/src/Message/Close.php | 51 ++ .../phrity/websocket/src/Message/Message.php | 113 +++ .../websocket/src/Message/MessageHandler.php | 129 +++ vendor/phrity/websocket/src/Message/Ping.php | 17 + vendor/phrity/websocket/src/Message/Pong.php | 17 + vendor/phrity/websocket/src/Message/Text.php | 27 + .../websocket/src/Middleware/Callback.php | 116 +++ .../websocket/src/Middleware/CloseHandler.php | 107 +++ .../src/Middleware/CompressionExtension.php | 185 +++++ .../CompressorInterface.php | 39 + .../DeflateCompressor.php | 250 ++++++ .../src/Middleware/FollowRedirect.php | 96 +++ .../src/Middleware/MiddlewareHandler.php | 204 +++++ .../src/Middleware/MiddlewareInterface.php | 18 + .../websocket/src/Middleware/PingInterval.php | 88 ++ .../src/Middleware/PingResponder.php | 61 ++ .../ProcessHttpIncomingInterface.php | 20 + .../ProcessHttpOutgoingInterface.php | 24 + .../src/Middleware/ProcessHttpStack.php | 70 ++ .../Middleware/ProcessIncomingInterface.php | 20 + .../Middleware/ProcessOutgoingInterface.php | 25 + .../websocket/src/Middleware/ProcessStack.php | 73 ++ .../src/Middleware/ProcessTickInterface.php | 19 + .../src/Middleware/ProcessTickStack.php | 47 ++ .../src/Middleware/SubprotocolNegotiation.php | 151 ++++ .../src/Runtime/IdentityInterface.php | 19 + .../phrity/websocket/src/Runtime/Runner.php | 94 +++ vendor/phrity/websocket/src/Server.php | 761 +++++++++++++++++ .../src/Trait/ConfigurationTrait.php | 35 + .../websocket/src/Trait/ListenerTrait.php | 126 +++ .../websocket/src/Trait/OpcodeTrait.php | 25 + .../websocket/src/Trait/SendMethodsTrait.php | 74 ++ .../websocket/src/Trait/StringableTrait.php | 25 + 144 files changed, 13983 insertions(+), 6 deletions(-) create mode 100644 vendor/nyholm/psr7/CHANGELOG.md create mode 100644 vendor/nyholm/psr7/LICENSE create mode 100644 vendor/nyholm/psr7/README.md create mode 100644 vendor/nyholm/psr7/composer.json create mode 100644 vendor/nyholm/psr7/src/Factory/HttplugFactory.php create mode 100644 vendor/nyholm/psr7/src/Factory/Psr17Factory.php create mode 100644 vendor/nyholm/psr7/src/MessageTrait.php create mode 100644 vendor/nyholm/psr7/src/Request.php create mode 100644 vendor/nyholm/psr7/src/RequestTrait.php create mode 100644 vendor/nyholm/psr7/src/Response.php create mode 100644 vendor/nyholm/psr7/src/ServerRequest.php create mode 100644 vendor/nyholm/psr7/src/Stream.php create mode 100644 vendor/nyholm/psr7/src/StreamTrait.php create mode 100644 vendor/nyholm/psr7/src/UploadedFile.php create mode 100644 vendor/nyholm/psr7/src/Uri.php create mode 100644 vendor/phrity/comparison/composer.json create mode 100644 vendor/phrity/comparison/src/Comparable.php create mode 100644 vendor/phrity/comparison/src/Comparator.php create mode 100644 vendor/phrity/comparison/src/ComparisonTrait.php create mode 100644 vendor/phrity/comparison/src/Equalable.php create mode 100644 vendor/phrity/comparison/src/IncomparableException.php create mode 100644 vendor/phrity/http/LICENSE create mode 100644 vendor/phrity/http/composer.json create mode 100644 vendor/phrity/http/src/HttpFactory.php create mode 100644 vendor/phrity/http/src/Serializer.php create mode 100644 vendor/phrity/net-stream/LICENSE create mode 100644 vendor/phrity/net-stream/composer.json create mode 100644 vendor/phrity/net-stream/src/Context.php create mode 100644 vendor/phrity/net-stream/src/SocketClient.php create mode 100644 vendor/phrity/net-stream/src/SocketServer.php create mode 100644 vendor/phrity/net-stream/src/SocketStream.php create mode 100644 vendor/phrity/net-stream/src/Stream.php create mode 100644 vendor/phrity/net-stream/src/StreamCollection.php create mode 100644 vendor/phrity/net-stream/src/StreamContainerInterface.php create mode 100644 vendor/phrity/net-stream/src/StreamException.php create mode 100644 vendor/phrity/net-stream/src/StreamFactory.php create mode 100644 vendor/phrity/net-stream/src/StreamInterface.php create mode 100644 vendor/phrity/net-uri/LICENSE create mode 100644 vendor/phrity/net-uri/composer.json create mode 100644 vendor/phrity/net-uri/src/Uri.php create mode 100644 vendor/phrity/net-uri/src/UriFactory.php create mode 100644 vendor/phrity/util-accessor/LICENSE create mode 100644 vendor/phrity/util-accessor/composer.json create mode 100644 vendor/phrity/util-accessor/src/Accessor.php create mode 100644 vendor/phrity/util-accessor/src/AccessorException.php create mode 100644 vendor/phrity/util-accessor/src/AccessorTrait.php create mode 100644 vendor/phrity/util-accessor/src/DataAccessor.php create mode 100644 vendor/phrity/util-accessor/src/PathAccessor.php create mode 100644 vendor/phrity/util-errorhandler/LICENSE create mode 100644 vendor/phrity/util-errorhandler/composer.json create mode 100644 vendor/phrity/util-errorhandler/src/ErrorHandler.php create mode 100644 vendor/phrity/util-interpolator/LICENSE create mode 100644 vendor/phrity/util-interpolator/composer.json create mode 100644 vendor/phrity/util-interpolator/src/Interpolator.php create mode 100644 vendor/phrity/util-interpolator/src/InterpolatorTrait.php create mode 100644 vendor/phrity/util-transformer/LICENSE create mode 100644 vendor/phrity/util-transformer/composer.json create mode 100644 vendor/phrity/util-transformer/src/BasicTypeConverter.php create mode 100644 vendor/phrity/util-transformer/src/ChainedResolver.php create mode 100644 vendor/phrity/util-transformer/src/DateTimeConverter.php create mode 100644 vendor/phrity/util-transformer/src/EnumConverter.php create mode 100644 vendor/phrity/util-transformer/src/FirstMatchResolver.php create mode 100644 vendor/phrity/util-transformer/src/FlattenDecoder.php create mode 100644 vendor/phrity/util-transformer/src/JsonDecoder.php create mode 100644 vendor/phrity/util-transformer/src/JsonSerializableConverter.php create mode 100644 vendor/phrity/util-transformer/src/ReadableConverter.php create mode 100644 vendor/phrity/util-transformer/src/RecursionResolver.php create mode 100644 vendor/phrity/util-transformer/src/ReversedReadableConverter.php create mode 100644 vendor/phrity/util-transformer/src/StringResolver.php create mode 100644 vendor/phrity/util-transformer/src/StringableConverter.php create mode 100644 vendor/phrity/util-transformer/src/Symfony/NormalizerWrapper.php create mode 100644 vendor/phrity/util-transformer/src/ThrowableConverter.php create mode 100644 vendor/phrity/util-transformer/src/TransformerException.php create mode 100644 vendor/phrity/util-transformer/src/TransformerInterface.php create mode 100644 vendor/phrity/util-transformer/src/Type.php create mode 100644 vendor/phrity/websocket/LICENSE.md create mode 100644 vendor/phrity/websocket/composer.json create mode 100644 vendor/phrity/websocket/src/Client.php create mode 100644 vendor/phrity/websocket/src/Configuration.php create mode 100644 vendor/phrity/websocket/src/Connection.php create mode 100644 vendor/phrity/websocket/src/Constant.php create mode 100644 vendor/phrity/websocket/src/Exception/BadOpcodeException.php create mode 100644 vendor/phrity/websocket/src/Exception/BadUriException.php create mode 100644 vendor/phrity/websocket/src/Exception/ClientException.php create mode 100644 vendor/phrity/websocket/src/Exception/CloseException.php create mode 100644 vendor/phrity/websocket/src/Exception/ConnectionClosedException.php create mode 100644 vendor/phrity/websocket/src/Exception/ConnectionFailureException.php create mode 100644 vendor/phrity/websocket/src/Exception/ConnectionLevelInterface.php create mode 100644 vendor/phrity/websocket/src/Exception/ConnectionTimeoutException.php create mode 100644 vendor/phrity/websocket/src/Exception/Exception.php create mode 100644 vendor/phrity/websocket/src/Exception/ExceptionInterface.php create mode 100644 vendor/phrity/websocket/src/Exception/HandshakeException.php create mode 100644 vendor/phrity/websocket/src/Exception/MessageLevelInterface.php create mode 100644 vendor/phrity/websocket/src/Exception/ReconnectException.php create mode 100644 vendor/phrity/websocket/src/Exception/RunnerException.php create mode 100644 vendor/phrity/websocket/src/Exception/ServerException.php create mode 100644 vendor/phrity/websocket/src/Frame/Frame.php create mode 100644 vendor/phrity/websocket/src/Frame/FrameHandler.php create mode 100644 vendor/phrity/websocket/src/Http/DefaultHttpFactory.php create mode 100644 vendor/phrity/websocket/src/Http/HttpHandler.php create mode 100644 vendor/phrity/websocket/src/Http/Request.php create mode 100644 vendor/phrity/websocket/src/Http/Response.php create mode 100644 vendor/phrity/websocket/src/Http/ServerRequest.php create mode 100644 vendor/phrity/websocket/src/Message/Binary.php create mode 100644 vendor/phrity/websocket/src/Message/Close.php create mode 100644 vendor/phrity/websocket/src/Message/Message.php create mode 100644 vendor/phrity/websocket/src/Message/MessageHandler.php create mode 100644 vendor/phrity/websocket/src/Message/Ping.php create mode 100644 vendor/phrity/websocket/src/Message/Pong.php create mode 100644 vendor/phrity/websocket/src/Message/Text.php create mode 100644 vendor/phrity/websocket/src/Middleware/Callback.php create mode 100644 vendor/phrity/websocket/src/Middleware/CloseHandler.php create mode 100644 vendor/phrity/websocket/src/Middleware/CompressionExtension.php create mode 100644 vendor/phrity/websocket/src/Middleware/CompressionExtension/CompressorInterface.php create mode 100644 vendor/phrity/websocket/src/Middleware/CompressionExtension/DeflateCompressor.php create mode 100644 vendor/phrity/websocket/src/Middleware/FollowRedirect.php create mode 100644 vendor/phrity/websocket/src/Middleware/MiddlewareHandler.php create mode 100644 vendor/phrity/websocket/src/Middleware/MiddlewareInterface.php create mode 100644 vendor/phrity/websocket/src/Middleware/PingInterval.php create mode 100644 vendor/phrity/websocket/src/Middleware/PingResponder.php create mode 100644 vendor/phrity/websocket/src/Middleware/ProcessHttpIncomingInterface.php create mode 100644 vendor/phrity/websocket/src/Middleware/ProcessHttpOutgoingInterface.php create mode 100644 vendor/phrity/websocket/src/Middleware/ProcessHttpStack.php create mode 100644 vendor/phrity/websocket/src/Middleware/ProcessIncomingInterface.php create mode 100644 vendor/phrity/websocket/src/Middleware/ProcessOutgoingInterface.php create mode 100644 vendor/phrity/websocket/src/Middleware/ProcessStack.php create mode 100644 vendor/phrity/websocket/src/Middleware/ProcessTickInterface.php create mode 100644 vendor/phrity/websocket/src/Middleware/ProcessTickStack.php create mode 100644 vendor/phrity/websocket/src/Middleware/SubprotocolNegotiation.php create mode 100644 vendor/phrity/websocket/src/Runtime/IdentityInterface.php create mode 100644 vendor/phrity/websocket/src/Runtime/Runner.php create mode 100644 vendor/phrity/websocket/src/Server.php create mode 100644 vendor/phrity/websocket/src/Trait/ConfigurationTrait.php create mode 100644 vendor/phrity/websocket/src/Trait/ListenerTrait.php create mode 100644 vendor/phrity/websocket/src/Trait/OpcodeTrait.php create mode 100644 vendor/phrity/websocket/src/Trait/SendMethodsTrait.php create mode 100644 vendor/phrity/websocket/src/Trait/StringableTrait.php diff --git a/app/Helpers/TrueNASWebSocketClient.php b/app/Helpers/TrueNASWebSocketClient.php index 17c32df08..0fb4a9865 100644 --- a/app/Helpers/TrueNASWebSocketClient.php +++ b/app/Helpers/TrueNASWebSocketClient.php @@ -5,7 +5,7 @@ use Illuminate\Support\Facades\Log; use Phrity\Net\Context; use WebSocket\Client; -use WebSocket\ConnectionException; +use WebSocket\Exception\Exception as WebSocketException; /** * TrueNAS JSON-RPC 2.0 WebSocket Client @@ -87,7 +87,7 @@ public function connect(): bool } throw new \Exception('Authentication failed: Invalid API key'); - } catch (ConnectionException $e) { + } catch (WebSocketException $e) { Log::error('TrueNAS WebSocket connection failed: ' . $e->getMessage()); $this->disconnect(); throw new \Exception('WebSocket connection failed: ' . $e->getMessage()); @@ -130,7 +130,7 @@ public function call(string $method, array $params = []) } return $decoded['result'] ?? null; - } catch (ConnectionException $e) { + } catch (WebSocketException $e) { Log::error('TrueNAS WebSocket call failed: ' . $e->getMessage()); throw new \Exception('WebSocket call failed: ' . $e->getMessage()); } diff --git a/composer.lock b/composer.lock index ccc071b59..6282c4f61 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "24b785f73e421451022a5e23d68cc6f0", + "content-hash": "90b6ed264d7efa958636840d86196b99", "packages": [ { "name": "aws/aws-crt-php", @@ -3536,6 +3536,84 @@ ], "time": "2026-02-16T23:10:27+00:00" }, + { + "name": "nyholm/psr7", + "version": "1.8.2", + "source": { + "type": "git", + "url": "https://github.com/Nyholm/psr7.git", + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Nyholm/psr7/zipball/a71f2b11690f4b24d099d6b16690a90ae14fc6f3", + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0" + }, + "provide": { + "php-http/message-factory-implementation": "1.0", + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "http-interop/http-factory-tests": "^0.9", + "php-http/message-factory": "^1.0", + "php-http/psr7-integration-tests": "^1.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.4", + "symfony/error-handler": "^4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.8-dev" + } + }, + "autoload": { + "psr-4": { + "Nyholm\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + }, + { + "name": "Martijn van der Ven", + "email": "martijn@vanderven.se" + } + ], + "description": "A fast PHP7 implementation of PSR-7", + "homepage": "https://tnyholm.se", + "keywords": [ + "psr-17", + "psr-7" + ], + "support": { + "issues": "https://github.com/Nyholm/psr7/issues", + "source": "https://github.com/Nyholm/psr7/tree/1.8.2" + }, + "funding": [ + { + "url": "https://github.com/Zegnat", + "type": "github" + }, + { + "url": "https://github.com/nyholm", + "type": "github" + } + ], + "time": "2024-09-09T07:06:30+00:00" + }, { "name": "php-http/cache-plugin", "version": "2.1.0", @@ -4046,6 +4124,526 @@ ], "time": "2025-12-27T19:41:33+00:00" }, + { + "name": "phrity/comparison", + "version": "1.4.1", + "source": { + "type": "git", + "url": "https://github.com/sirn-se/phrity-comparison.git", + "reference": "cf80abb822537eeaaeb4142157cd667ca6372a13" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sirn-se/phrity-comparison/zipball/cf80abb822537eeaaeb4142157cd667ca6372a13", + "reference": "cf80abb822537eeaaeb4142157cd667ca6372a13", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", + "robiningelbrecht/phpunit-coverage-tools": "^1.9", + "squizlabs/php_codesniffer": "^3.5 || ^4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Phrity\\Comparison\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sören Jensen", + "email": "sirn@sirn.se", + "homepage": "https://phrity.sirn.se" + } + ], + "description": "Interfaces and helper trait for comparing objects. Comparator for sort and filter applications.", + "homepage": "https://phrity.sirn.se/comparison", + "keywords": [ + "comparable", + "comparator", + "comparison", + "equalable", + "filter", + "sort" + ], + "support": { + "issues": "https://github.com/sirn-se/phrity-comparison/issues", + "source": "https://github.com/sirn-se/phrity-comparison/tree/1.4.1" + }, + "time": "2025-12-05T07:38:30+00:00" + }, + { + "name": "phrity/http", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/sirn-se/phrity-http.git", + "reference": "1e7eee67359287b94aae2b7d40b730d5f5394943" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sirn-se/phrity-http/zipball/1e7eee67359287b94aae2b7d40b730d5f5394943", + "reference": "1e7eee67359287b94aae2b7d40b730d5f5394943", + "shasum": "" + }, + "require": { + "php": "^8.1", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0" + }, + "require-dev": { + "guzzlehttp/psr7": "^2.0", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", + "robiningelbrecht/phpunit-coverage-tools": "^1.9", + "squizlabs/php_codesniffer": "^3.5 || ^4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Phrity\\Http\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sören Jensen", + "email": "sirn@sirn.se", + "homepage": "https://phrity.sirn.se" + } + ], + "description": "Utilities and interfaces for handling HTTP.", + "homepage": "https://phrity.sirn.se/http", + "keywords": [ + "HTTP Factories", + "HTTP Serializer", + "http", + "psr-17", + "psr-7" + ], + "support": { + "issues": "https://github.com/sirn-se/phrity-http/issues", + "source": "https://github.com/sirn-se/phrity-http/tree/1.1.0" + }, + "time": "2025-12-22T20:22:29+00:00" + }, + { + "name": "phrity/net-stream", + "version": "2.4.0", + "source": { + "type": "git", + "url": "https://github.com/sirn-se/phrity-net-stream.git", + "reference": "a0726a8dddf11a4cd3a9e45d17e145d78e948f43" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sirn-se/phrity-net-stream/zipball/a0726a8dddf11a4cd3a9e45d17e145d78e948f43", + "reference": "a0726a8dddf11a4cd3a9e45d17e145d78e948f43", + "shasum": "" + }, + "require": { + "php": "^8.1", + "phrity/util-errorhandler": "^1.1", + "phrity/util-interpolator": "^1.1", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0 || ^13.0", + "phrity/net-uri": "^2.0", + "robiningelbrecht/phpunit-coverage-tools": "^1.9", + "squizlabs/php_codesniffer": "^4.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": "Socket stream classes implementing PSR-7 Stream and PSR-17 StreamFactory", + "homepage": "https://phrity.sirn.se/net-stream", + "keywords": [ + "Socket", + "client", + "psr-17", + "psr-7", + "server", + "stream", + "stream factory" + ], + "support": { + "issues": "https://github.com/sirn-se/phrity-net-stream/issues", + "source": "https://github.com/sirn-se/phrity-net-stream/tree/2.4.0" + }, + "time": "2026-03-25T18:16:40+00:00" + }, + { + "name": "phrity/net-uri", + "version": "2.2.1", + "source": { + "type": "git", + "url": "https://github.com/sirn-se/phrity-net-uri.git", + "reference": "0737de026b75177ae302ac9fdbbd0ffc2610f3b8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sirn-se/phrity-net-uri/zipball/0737de026b75177ae302ac9fdbbd0ffc2610f3b8", + "reference": "0737de026b75177ae302ac9fdbbd0ffc2610f3b8", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^8.1", + "phrity/comparison": "^1.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 | ^2.0" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", + "phrity/util-errorhandler": "^1.1", + "robiningelbrecht/phpunit-coverage-tools": "^1.9", + "squizlabs/php_codesniffer": "^3.5 || ^4.0" + }, + "suggest": { + "ext-intl": "Enables IDN conversion for non-ASCII domains" + }, + "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/2.2.1" + }, + "time": "2025-12-05T10:39:22+00:00" + }, + { + "name": "phrity/util-accessor", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/sirn-se/phrity-util-accessor.git", + "reference": "e9741da6b532fa2da7f627fce60fa77f00f6e354" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sirn-se/phrity-util-accessor/zipball/e9741da6b532fa2da7f627fce60fa77f00f6e354", + "reference": "e9741da6b532fa2da7f627fce60fa77f00f6e354", + "shasum": "" + }, + "require": { + "php": "^8.1", + "phrity/util-transformer": "^1.0" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", + "robiningelbrecht/phpunit-coverage-tools": "^1.9", + "squizlabs/php_codesniffer": "^3.5 || ^4.0" + }, + "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": "Utility to handle access to a data set by using access paths. Similar to and partly compatible with xpath and json-pointer.", + "homepage": "https://phrity.sirn.se/util-accessor", + "keywords": [ + "accessor" + ], + "support": { + "issues": "https://github.com/sirn-se/phrity-util-accessor/issues", + "source": "https://github.com/sirn-se/phrity-util-accessor/tree/1.3.1" + }, + "time": "2025-12-05T21:18:15+00:00" + }, + { + "name": "phrity/util-errorhandler", + "version": "1.2.2", + "source": { + "type": "git", + "url": "https://github.com/sirn-se/phrity-util-errorhandler.git", + "reference": "70a669cc22db2eed6a109ec66fd95168a4332c9b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sirn-se/phrity-util-errorhandler/zipball/70a669cc22db2eed6a109ec66fd95168a4332c9b", + "reference": "70a669cc22db2eed6a109ec66fd95168a4332c9b", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", + "robiningelbrecht/phpunit-coverage-tools": "^1.9", + "squizlabs/php_codesniffer": "^3.5 || ^4.0" + }, + "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.2.2" + }, + "time": "2025-12-05T21:25:36+00:00" + }, + { + "name": "phrity/util-interpolator", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/sirn-se/phrity-util-interpolator.git", + "reference": "18b0362d2aff60984c530808333c5e64c80c3e50" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sirn-se/phrity-util-interpolator/zipball/18b0362d2aff60984c530808333c5e64c80c3e50", + "reference": "18b0362d2aff60984c530808333c5e64c80c3e50", + "shasum": "" + }, + "require": { + "php": "^8.1", + "phrity/util-accessor": "^1.3", + "phrity/util-transformer": "^1.3" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", + "robiningelbrecht/phpunit-coverage-tools": "^1.9", + "squizlabs/php_codesniffer": "^3.5 || ^4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Phrity\\Util\\Interpolator\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sören Jensen", + "email": "sirn@sirn.se", + "homepage": "https://phrity.sirn.se" + } + ], + "description": "Interpolator class and trait.", + "homepage": "https://phrity.sirn.se/util-interpolator", + "keywords": [ + "interpolate", + "interpolator" + ], + "support": { + "issues": "https://github.com/sirn-se/phrity-util-interpolator/issues", + "source": "https://github.com/sirn-se/phrity-util-interpolator/tree/1.1.1" + }, + "time": "2025-12-05T21:35:25+00:00" + }, + { + "name": "phrity/util-transformer", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/sirn-se/phrity-util-transformer.git", + "reference": "3fd4d7fa4077deafe5130c4b9ee8146b5488abe3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sirn-se/phrity-util-transformer/zipball/3fd4d7fa4077deafe5130c4b9ee8146b5488abe3", + "reference": "3fd4d7fa4077deafe5130c4b9ee8146b5488abe3", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", + "robiningelbrecht/phpunit-coverage-tools": "^1.9", + "squizlabs/php_codesniffer": "^3.5 || ^4.0", + "symfony/property-access": "^6.0 || ^7.0 || ^8.0", + "symfony/serializer": "^6.0 || ^7.0 || ^8.0", + "symfony/translation-contracts": "^3.0", + "symfony/uid": "^6.0 || ^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Phrity\\Util\\Transformer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sören Jensen", + "email": "sirn@sirn.se", + "homepage": "https://phrity.sirn.se" + } + ], + "description": "Type transformers, normalizers and resolvers.", + "homepage": "https://phrity.sirn.se/util-transformer", + "keywords": [ + "normalizer", + "resolver", + "transformer", + "type" + ], + "support": { + "issues": "https://github.com/sirn-se/phrity-util-transformer/issues", + "source": "https://github.com/sirn-se/phrity-util-transformer/tree/1.3.1" + }, + "time": "2025-12-05T22:07:04+00:00" + }, + { + "name": "phrity/websocket", + "version": "3.7.3", + "source": { + "type": "git", + "url": "https://github.com/sirn-se/websocket-php.git", + "reference": "c92d613ec298ea108b8ebae306609cd40427f7d6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sirn-se/websocket-php/zipball/c92d613ec298ea108b8ebae306609cd40427f7d6", + "reference": "c92d613ec298ea108b8ebae306609cd40427f7d6", + "shasum": "" + }, + "require": { + "nyholm/psr7": "^1.4", + "php": "^8.1", + "phrity/http": "^1.1", + "phrity/net-stream": "^2.4", + "phrity/net-uri": "^2.1", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "psr/log": "^1.0 || ^2.0 || ^3.0" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0 || ^13.0", + "phrity/logger-console": "^1.0", + "phrity/net-mock": "^2.4", + "phrity/util-errorhandler": "^1.1", + "robiningelbrecht/phpunit-coverage-tools": "^1.9", + "squizlabs/php_codesniffer": "^4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "WebSocket\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Fredrik Liljegren" + }, + { + "name": "Sören Jensen", + "email": "sirn@sirn.se", + "homepage": "https://phrity.sirn.se" + } + ], + "description": "WebSocket client and server", + "homepage": "https://phrity.sirn.se/websocket", + "keywords": [ + "client", + "server", + "websocket" + ], + "support": { + "issues": "https://github.com/sirn-se/websocket-php/issues", + "source": "https://github.com/sirn-se/websocket-php/tree/3.7.3" + }, + "time": "2026-06-22T14:53:45+00:00" + }, { "name": "psr/cache", "version": "3.0.0", diff --git a/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php index 17d40ef9f..7dea02d9a 100644 --- a/vendor/composer/autoload_classmap.php +++ b/vendor/composer/autoload_classmap.php @@ -33,6 +33,7 @@ 'App\\Console\\Commands\\RegisterApp' => $baseDir . '/app/Console/Commands/RegisterApp.php', 'App\\EnhancedApps' => $baseDir . '/app/EnhancedApps.php', 'App\\Facades\\Form' => $baseDir . '/app/Facades/Form.php', + 'App\\Helpers\\TrueNASWebSocketClient' => $baseDir . '/app/Helpers/TrueNASWebSocketClient.php', 'App\\Http\\Controllers\\Auth\\ForgotPasswordController' => $baseDir . '/app/Http/Controllers/Auth/ForgotPasswordController.php', 'App\\Http\\Controllers\\Auth\\LoginController' => $baseDir . '/app/Http/Controllers/Auth/LoginController.php', 'App\\Http\\Controllers\\Auth\\RegisterController' => $baseDir . '/app/Http/Controllers/Auth/RegisterController.php', @@ -5199,6 +5200,17 @@ 'NunoMaduro\\Collision\\Provider' => $vendorDir . '/nunomaduro/collision/src/Provider.php', 'NunoMaduro\\Collision\\SolutionsRepositories\\NullSolutionsRepository' => $vendorDir . '/nunomaduro/collision/src/SolutionsRepositories/NullSolutionsRepository.php', 'NunoMaduro\\Collision\\Writer' => $vendorDir . '/nunomaduro/collision/src/Writer.php', + 'Nyholm\\Psr7\\Factory\\HttplugFactory' => $vendorDir . '/nyholm/psr7/src/Factory/HttplugFactory.php', + 'Nyholm\\Psr7\\Factory\\Psr17Factory' => $vendorDir . '/nyholm/psr7/src/Factory/Psr17Factory.php', + 'Nyholm\\Psr7\\MessageTrait' => $vendorDir . '/nyholm/psr7/src/MessageTrait.php', + 'Nyholm\\Psr7\\Request' => $vendorDir . '/nyholm/psr7/src/Request.php', + 'Nyholm\\Psr7\\RequestTrait' => $vendorDir . '/nyholm/psr7/src/RequestTrait.php', + 'Nyholm\\Psr7\\Response' => $vendorDir . '/nyholm/psr7/src/Response.php', + 'Nyholm\\Psr7\\ServerRequest' => $vendorDir . '/nyholm/psr7/src/ServerRequest.php', + 'Nyholm\\Psr7\\Stream' => $vendorDir . '/nyholm/psr7/src/Stream.php', + 'Nyholm\\Psr7\\StreamTrait' => $vendorDir . '/nyholm/psr7/src/StreamTrait.php', + 'Nyholm\\Psr7\\UploadedFile' => $vendorDir . '/nyholm/psr7/src/UploadedFile.php', + 'Nyholm\\Psr7\\Uri' => $vendorDir . '/nyholm/psr7/src/Uri.php', 'PHPUnit\\Event\\Application\\Finished' => $vendorDir . '/phpunit/phpunit/src/Event/Events/Application/Finished.php', 'PHPUnit\\Event\\Application\\FinishedSubscriber' => $vendorDir . '/phpunit/phpunit/src/Event/Events/Application/FinishedSubscriber.php', 'PHPUnit\\Event\\Application\\Started' => $vendorDir . '/phpunit/phpunit/src/Event/Events/Application/Started.php', @@ -6553,6 +6565,51 @@ 'PhpParser\\PrettyPrinter\\Standard' => $vendorDir . '/nikic/php-parser/lib/PhpParser/PrettyPrinter/Standard.php', 'PhpParser\\Token' => $vendorDir . '/nikic/php-parser/lib/PhpParser/Token.php', 'PhpToken' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php', + 'Phrity\\Comparison\\Comparable' => $vendorDir . '/phrity/comparison/src/Comparable.php', + 'Phrity\\Comparison\\Comparator' => $vendorDir . '/phrity/comparison/src/Comparator.php', + 'Phrity\\Comparison\\ComparisonTrait' => $vendorDir . '/phrity/comparison/src/ComparisonTrait.php', + 'Phrity\\Comparison\\Equalable' => $vendorDir . '/phrity/comparison/src/Equalable.php', + 'Phrity\\Comparison\\IncomparableException' => $vendorDir . '/phrity/comparison/src/IncomparableException.php', + 'Phrity\\Http\\HttpFactory' => $vendorDir . '/phrity/http/src/HttpFactory.php', + 'Phrity\\Http\\Serializer' => $vendorDir . '/phrity/http/src/Serializer.php', + 'Phrity\\Net\\Context' => $vendorDir . '/phrity/net-stream/src/Context.php', + 'Phrity\\Net\\SocketClient' => $vendorDir . '/phrity/net-stream/src/SocketClient.php', + 'Phrity\\Net\\SocketServer' => $vendorDir . '/phrity/net-stream/src/SocketServer.php', + 'Phrity\\Net\\SocketStream' => $vendorDir . '/phrity/net-stream/src/SocketStream.php', + 'Phrity\\Net\\Stream' => $vendorDir . '/phrity/net-stream/src/Stream.php', + 'Phrity\\Net\\StreamCollection' => $vendorDir . '/phrity/net-stream/src/StreamCollection.php', + 'Phrity\\Net\\StreamContainerInterface' => $vendorDir . '/phrity/net-stream/src/StreamContainerInterface.php', + 'Phrity\\Net\\StreamException' => $vendorDir . '/phrity/net-stream/src/StreamException.php', + 'Phrity\\Net\\StreamFactory' => $vendorDir . '/phrity/net-stream/src/StreamFactory.php', + 'Phrity\\Net\\StreamInterface' => $vendorDir . '/phrity/net-stream/src/StreamInterface.php', + 'Phrity\\Net\\Uri' => $vendorDir . '/phrity/net-uri/src/Uri.php', + 'Phrity\\Net\\UriFactory' => $vendorDir . '/phrity/net-uri/src/UriFactory.php', + 'Phrity\\Util\\Accessor' => $vendorDir . '/phrity/util-accessor/src/Accessor.php', + 'Phrity\\Util\\AccessorException' => $vendorDir . '/phrity/util-accessor/src/AccessorException.php', + 'Phrity\\Util\\AccessorTrait' => $vendorDir . '/phrity/util-accessor/src/AccessorTrait.php', + 'Phrity\\Util\\DataAccessor' => $vendorDir . '/phrity/util-accessor/src/DataAccessor.php', + 'Phrity\\Util\\ErrorHandler' => $vendorDir . '/phrity/util-errorhandler/src/ErrorHandler.php', + 'Phrity\\Util\\Interpolator\\Interpolator' => $vendorDir . '/phrity/util-interpolator/src/Interpolator.php', + 'Phrity\\Util\\Interpolator\\InterpolatorTrait' => $vendorDir . '/phrity/util-interpolator/src/InterpolatorTrait.php', + 'Phrity\\Util\\PathAccessor' => $vendorDir . '/phrity/util-accessor/src/PathAccessor.php', + 'Phrity\\Util\\Transformer\\BasicTypeConverter' => $vendorDir . '/phrity/util-transformer/src/BasicTypeConverter.php', + 'Phrity\\Util\\Transformer\\ChainedResolver' => $vendorDir . '/phrity/util-transformer/src/ChainedResolver.php', + 'Phrity\\Util\\Transformer\\DateTimeConverter' => $vendorDir . '/phrity/util-transformer/src/DateTimeConverter.php', + 'Phrity\\Util\\Transformer\\EnumConverter' => $vendorDir . '/phrity/util-transformer/src/EnumConverter.php', + 'Phrity\\Util\\Transformer\\FirstMatchResolver' => $vendorDir . '/phrity/util-transformer/src/FirstMatchResolver.php', + 'Phrity\\Util\\Transformer\\FlattenDecoder' => $vendorDir . '/phrity/util-transformer/src/FlattenDecoder.php', + 'Phrity\\Util\\Transformer\\JsonDecoder' => $vendorDir . '/phrity/util-transformer/src/JsonDecoder.php', + 'Phrity\\Util\\Transformer\\JsonSerializableConverter' => $vendorDir . '/phrity/util-transformer/src/JsonSerializableConverter.php', + 'Phrity\\Util\\Transformer\\ReadableConverter' => $vendorDir . '/phrity/util-transformer/src/ReadableConverter.php', + 'Phrity\\Util\\Transformer\\RecursionResolver' => $vendorDir . '/phrity/util-transformer/src/RecursionResolver.php', + 'Phrity\\Util\\Transformer\\ReversedReadableConverter' => $vendorDir . '/phrity/util-transformer/src/ReversedReadableConverter.php', + 'Phrity\\Util\\Transformer\\StringResolver' => $vendorDir . '/phrity/util-transformer/src/StringResolver.php', + 'Phrity\\Util\\Transformer\\StringableConverter' => $vendorDir . '/phrity/util-transformer/src/StringableConverter.php', + 'Phrity\\Util\\Transformer\\Symfony\\NormalizerWrapper' => $vendorDir . '/phrity/util-transformer/src/Symfony/NormalizerWrapper.php', + 'Phrity\\Util\\Transformer\\ThrowableConverter' => $vendorDir . '/phrity/util-transformer/src/ThrowableConverter.php', + 'Phrity\\Util\\Transformer\\TransformerException' => $vendorDir . '/phrity/util-transformer/src/TransformerException.php', + 'Phrity\\Util\\Transformer\\TransformerInterface' => $vendorDir . '/phrity/util-transformer/src/TransformerInterface.php', + 'Phrity\\Util\\Transformer\\Type' => $vendorDir . '/phrity/util-transformer/src/Type.php', 'Psr\\Cache\\CacheException' => $vendorDir . '/psr/cache/src/CacheException.php', 'Psr\\Cache\\CacheItemInterface' => $vendorDir . '/psr/cache/src/CacheItemInterface.php', 'Psr\\Cache\\CacheItemPoolInterface' => $vendorDir . '/psr/cache/src/CacheItemPoolInterface.php', @@ -8851,6 +8908,8 @@ 'Termwind\\ValueObjects\\Node' => $vendorDir . '/nunomaduro/termwind/src/ValueObjects/Node.php', 'Termwind\\ValueObjects\\Style' => $vendorDir . '/nunomaduro/termwind/src/ValueObjects/Style.php', 'Termwind\\ValueObjects\\Styles' => $vendorDir . '/nunomaduro/termwind/src/ValueObjects/Styles.php', + 'Tests\\Feature\\AjaxPostEndpointsTest' => $baseDir . '/tests/Feature/AjaxPostEndpointsTest.php', + 'Tests\\Feature\\CsrfExceptionsTest' => $baseDir . '/tests/Feature/CsrfExceptionsTest.php', 'Tests\\Feature\\DashTest' => $baseDir . '/tests/Feature/DashTest.php', 'Tests\\Feature\\GetStatsTest' => $baseDir . '/tests/Feature/GetStatsTest.php', 'Tests\\Feature\\ImportTest' => $baseDir . '/tests/Feature/ImportTest.php', @@ -8859,16 +8918,24 @@ 'Tests\\Feature\\ItemExportTest' => $baseDir . '/tests/Feature/ItemExportTest.php', 'Tests\\Feature\\ItemImportTest' => $baseDir . '/tests/Feature/ItemImportTest.php', 'Tests\\Feature\\ItemListTest' => $baseDir . '/tests/Feature/ItemListTest.php', + 'Tests\\Feature\\ItemOwnershipTest' => $baseDir . '/tests/Feature/ItemOwnershipTest.php', + 'Tests\\Feature\\OrphanItemRecoveryTest' => $baseDir . '/tests/Feature/OrphanItemRecoveryTest.php', + 'Tests\\Feature\\RoutesRenderTest' => $baseDir . '/tests/Feature/RoutesRenderTest.php', 'Tests\\Feature\\SearchTest' => $baseDir . '/tests/Feature/SearchTest.php', 'Tests\\Feature\\SettingsTest' => $baseDir . '/tests/Feature/SettingsTest.php', + 'Tests\\Feature\\StorageDiskTest' => $baseDir . '/tests/Feature/StorageDiskTest.php', 'Tests\\Feature\\TagListTest' => $baseDir . '/tests/Feature/TagListTest.php', 'Tests\\Feature\\TrustHostsTest' => $baseDir . '/tests/Feature/TrustHostsTest.php', 'Tests\\Feature\\TrustProxiesTest' => $baseDir . '/tests/Feature/TrustProxiesTest.php', + 'Tests\\Feature\\UserDeleteItemsTest' => $baseDir . '/tests/Feature/UserDeleteItemsTest.php', 'Tests\\Feature\\UserEditTest' => $baseDir . '/tests/Feature/UserEditTest.php', 'Tests\\Feature\\UserListTest' => $baseDir . '/tests/Feature/UserListTest.php', 'Tests\\TestCase' => $baseDir . '/tests/TestCase.php', 'Tests\\Unit\\database\\seeders\\SettingsSeederTest' => $baseDir . '/tests/Unit/database/seeders/SettingsSeederTest.php', + 'Tests\\Unit\\helpers\\ClassNameTest' => $baseDir . '/tests/Unit/helpers/ClassNameTest.php', + 'Tests\\Unit\\helpers\\ColorHelpersTest' => $baseDir . '/tests/Unit/helpers/ColorHelpersTest.php', 'Tests\\Unit\\helpers\\IsImageTest' => $baseDir . '/tests/Unit/helpers/IsImageTest.php', + 'Tests\\Unit\\helpers\\SizeHelpersTest' => $baseDir . '/tests/Unit/helpers/SizeHelpersTest.php', 'Tests\\Unit\\helpers\\SlugTest' => $baseDir . '/tests/Unit/helpers/SlugTest.php', 'Tests\\Unit\\lang\\LangTest' => $baseDir . '/tests/Unit/lang/LangTest.php', 'TheSeer\\Tokenizer\\Exception' => $vendorDir . '/theseer/tokenizer/src/Exception.php', @@ -8887,6 +8954,66 @@ 'TijsVerkoyen\\CssToInlineStyles\\Css\\Rule\\Rule' => $vendorDir . '/tijsverkoyen/css-to-inline-styles/src/Css/Rule/Rule.php', 'UnhandledMatchError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php', 'ValueError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/ValueError.php', + 'WebSocket\\Client' => $vendorDir . '/phrity/websocket/src/Client.php', + 'WebSocket\\Configuration' => $vendorDir . '/phrity/websocket/src/Configuration.php', + 'WebSocket\\Connection' => $vendorDir . '/phrity/websocket/src/Connection.php', + 'WebSocket\\Constant' => $vendorDir . '/phrity/websocket/src/Constant.php', + 'WebSocket\\Exception\\BadOpcodeException' => $vendorDir . '/phrity/websocket/src/Exception/BadOpcodeException.php', + 'WebSocket\\Exception\\BadUriException' => $vendorDir . '/phrity/websocket/src/Exception/BadUriException.php', + 'WebSocket\\Exception\\ClientException' => $vendorDir . '/phrity/websocket/src/Exception/ClientException.php', + 'WebSocket\\Exception\\CloseException' => $vendorDir . '/phrity/websocket/src/Exception/CloseException.php', + 'WebSocket\\Exception\\ConnectionClosedException' => $vendorDir . '/phrity/websocket/src/Exception/ConnectionClosedException.php', + 'WebSocket\\Exception\\ConnectionFailureException' => $vendorDir . '/phrity/websocket/src/Exception/ConnectionFailureException.php', + 'WebSocket\\Exception\\ConnectionLevelInterface' => $vendorDir . '/phrity/websocket/src/Exception/ConnectionLevelInterface.php', + 'WebSocket\\Exception\\ConnectionTimeoutException' => $vendorDir . '/phrity/websocket/src/Exception/ConnectionTimeoutException.php', + 'WebSocket\\Exception\\Exception' => $vendorDir . '/phrity/websocket/src/Exception/Exception.php', + 'WebSocket\\Exception\\ExceptionInterface' => $vendorDir . '/phrity/websocket/src/Exception/ExceptionInterface.php', + 'WebSocket\\Exception\\HandshakeException' => $vendorDir . '/phrity/websocket/src/Exception/HandshakeException.php', + 'WebSocket\\Exception\\MessageLevelInterface' => $vendorDir . '/phrity/websocket/src/Exception/MessageLevelInterface.php', + 'WebSocket\\Exception\\ReconnectException' => $vendorDir . '/phrity/websocket/src/Exception/ReconnectException.php', + 'WebSocket\\Exception\\RunnerException' => $vendorDir . '/phrity/websocket/src/Exception/RunnerException.php', + 'WebSocket\\Exception\\ServerException' => $vendorDir . '/phrity/websocket/src/Exception/ServerException.php', + 'WebSocket\\Frame\\Frame' => $vendorDir . '/phrity/websocket/src/Frame/Frame.php', + 'WebSocket\\Frame\\FrameHandler' => $vendorDir . '/phrity/websocket/src/Frame/FrameHandler.php', + 'WebSocket\\Http\\DefaultHttpFactory' => $vendorDir . '/phrity/websocket/src/Http/DefaultHttpFactory.php', + 'WebSocket\\Http\\HttpHandler' => $vendorDir . '/phrity/websocket/src/Http/HttpHandler.php', + 'WebSocket\\Http\\Request' => $vendorDir . '/phrity/websocket/src/Http/Request.php', + 'WebSocket\\Http\\Response' => $vendorDir . '/phrity/websocket/src/Http/Response.php', + 'WebSocket\\Http\\ServerRequest' => $vendorDir . '/phrity/websocket/src/Http/ServerRequest.php', + 'WebSocket\\Message\\Binary' => $vendorDir . '/phrity/websocket/src/Message/Binary.php', + 'WebSocket\\Message\\Close' => $vendorDir . '/phrity/websocket/src/Message/Close.php', + 'WebSocket\\Message\\Message' => $vendorDir . '/phrity/websocket/src/Message/Message.php', + 'WebSocket\\Message\\MessageHandler' => $vendorDir . '/phrity/websocket/src/Message/MessageHandler.php', + 'WebSocket\\Message\\Ping' => $vendorDir . '/phrity/websocket/src/Message/Ping.php', + 'WebSocket\\Message\\Pong' => $vendorDir . '/phrity/websocket/src/Message/Pong.php', + 'WebSocket\\Message\\Text' => $vendorDir . '/phrity/websocket/src/Message/Text.php', + 'WebSocket\\Middleware\\Callback' => $vendorDir . '/phrity/websocket/src/Middleware/Callback.php', + 'WebSocket\\Middleware\\CloseHandler' => $vendorDir . '/phrity/websocket/src/Middleware/CloseHandler.php', + 'WebSocket\\Middleware\\CompressionExtension' => $vendorDir . '/phrity/websocket/src/Middleware/CompressionExtension.php', + 'WebSocket\\Middleware\\CompressionExtension\\CompressorInterface' => $vendorDir . '/phrity/websocket/src/Middleware/CompressionExtension/CompressorInterface.php', + 'WebSocket\\Middleware\\CompressionExtension\\DeflateCompressor' => $vendorDir . '/phrity/websocket/src/Middleware/CompressionExtension/DeflateCompressor.php', + 'WebSocket\\Middleware\\FollowRedirect' => $vendorDir . '/phrity/websocket/src/Middleware/FollowRedirect.php', + 'WebSocket\\Middleware\\MiddlewareHandler' => $vendorDir . '/phrity/websocket/src/Middleware/MiddlewareHandler.php', + 'WebSocket\\Middleware\\MiddlewareInterface' => $vendorDir . '/phrity/websocket/src/Middleware/MiddlewareInterface.php', + 'WebSocket\\Middleware\\PingInterval' => $vendorDir . '/phrity/websocket/src/Middleware/PingInterval.php', + 'WebSocket\\Middleware\\PingResponder' => $vendorDir . '/phrity/websocket/src/Middleware/PingResponder.php', + 'WebSocket\\Middleware\\ProcessHttpIncomingInterface' => $vendorDir . '/phrity/websocket/src/Middleware/ProcessHttpIncomingInterface.php', + 'WebSocket\\Middleware\\ProcessHttpOutgoingInterface' => $vendorDir . '/phrity/websocket/src/Middleware/ProcessHttpOutgoingInterface.php', + 'WebSocket\\Middleware\\ProcessHttpStack' => $vendorDir . '/phrity/websocket/src/Middleware/ProcessHttpStack.php', + 'WebSocket\\Middleware\\ProcessIncomingInterface' => $vendorDir . '/phrity/websocket/src/Middleware/ProcessIncomingInterface.php', + 'WebSocket\\Middleware\\ProcessOutgoingInterface' => $vendorDir . '/phrity/websocket/src/Middleware/ProcessOutgoingInterface.php', + 'WebSocket\\Middleware\\ProcessStack' => $vendorDir . '/phrity/websocket/src/Middleware/ProcessStack.php', + 'WebSocket\\Middleware\\ProcessTickInterface' => $vendorDir . '/phrity/websocket/src/Middleware/ProcessTickInterface.php', + 'WebSocket\\Middleware\\ProcessTickStack' => $vendorDir . '/phrity/websocket/src/Middleware/ProcessTickStack.php', + 'WebSocket\\Middleware\\SubprotocolNegotiation' => $vendorDir . '/phrity/websocket/src/Middleware/SubprotocolNegotiation.php', + 'WebSocket\\Runtime\\IdentityInterface' => $vendorDir . '/phrity/websocket/src/Runtime/IdentityInterface.php', + 'WebSocket\\Runtime\\Runner' => $vendorDir . '/phrity/websocket/src/Runtime/Runner.php', + 'WebSocket\\Server' => $vendorDir . '/phrity/websocket/src/Server.php', + 'WebSocket\\Trait\\ConfigurationTrait' => $vendorDir . '/phrity/websocket/src/Trait/ConfigurationTrait.php', + 'WebSocket\\Trait\\ListenerTrait' => $vendorDir . '/phrity/websocket/src/Trait/ListenerTrait.php', + 'WebSocket\\Trait\\OpcodeTrait' => $vendorDir . '/phrity/websocket/src/Trait/OpcodeTrait.php', + 'WebSocket\\Trait\\SendMethodsTrait' => $vendorDir . '/phrity/websocket/src/Trait/SendMethodsTrait.php', + 'WebSocket\\Trait\\StringableTrait' => $vendorDir . '/phrity/websocket/src/Trait/StringableTrait.php', 'Whoops\\Exception\\ErrorException' => $vendorDir . '/filp/whoops/src/Whoops/Exception/ErrorException.php', 'Whoops\\Exception\\Formatter' => $vendorDir . '/filp/whoops/src/Whoops/Exception/Formatter.php', 'Whoops\\Exception\\Frame' => $vendorDir . '/filp/whoops/src/Whoops/Exception/Frame.php', diff --git a/vendor/composer/autoload_psr4.php b/vendor/composer/autoload_psr4.php index e2d1f2ab4..7dcf5c2fe 100644 --- a/vendor/composer/autoload_psr4.php +++ b/vendor/composer/autoload_psr4.php @@ -9,6 +9,7 @@ 'voku\\' => array($vendorDir . '/voku/portable-ascii/src/voku'), 'enshrined\\svgSanitize\\' => array($vendorDir . '/enshrined/svg-sanitize/src'), 'Whoops\\' => array($vendorDir . '/filp/whoops/src/Whoops'), + 'WebSocket\\' => array($vendorDir . '/phrity/websocket/src'), 'TijsVerkoyen\\CssToInlineStyles\\' => array($vendorDir . '/tijsverkoyen/css-to-inline-styles/src'), 'Tests\\' => array($baseDir . '/tests'), 'Termwind\\' => array($vendorDir . '/nunomaduro/termwind/src'), @@ -66,8 +67,15 @@ 'Psr\\Container\\' => array($vendorDir . '/psr/container/src'), 'Psr\\Clock\\' => array($vendorDir . '/psr/clock/src'), 'Psr\\Cache\\' => array($vendorDir . '/psr/cache/src'), + 'Phrity\\Util\\Transformer\\' => array($vendorDir . '/phrity/util-transformer/src'), + 'Phrity\\Util\\Interpolator\\' => array($vendorDir . '/phrity/util-interpolator/src'), + 'Phrity\\Util\\' => array($vendorDir . '/phrity/util-errorhandler/src', $vendorDir . '/phrity/util-accessor/src'), + 'Phrity\\Net\\' => array($vendorDir . '/phrity/net-uri/src', $vendorDir . '/phrity/net-stream/src'), + 'Phrity\\Http\\' => array($vendorDir . '/phrity/http/src'), + 'Phrity\\Comparison\\' => array($vendorDir . '/phrity/comparison/src'), 'PhpParser\\' => array($vendorDir . '/nikic/php-parser/lib/PhpParser'), 'PhpOption\\' => array($vendorDir . '/phpoption/phpoption/src/PhpOption'), + 'Nyholm\\Psr7\\' => array($vendorDir . '/nyholm/psr7/src'), 'NunoMaduro\\Collision\\' => array($vendorDir . '/nunomaduro/collision/src'), 'Nette\\' => array($vendorDir . '/nette/schema/src', $vendorDir . '/nette/utils/src'), 'Monolog\\' => array($vendorDir . '/monolog/monolog/src/Monolog'), diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php index 2a51a2fd7..46b8b6114 100644 --- a/vendor/composer/autoload_static.php +++ b/vendor/composer/autoload_static.php @@ -65,6 +65,7 @@ class ComposerStaticInitb2555e5ff7197b9e020da74bbd3b7cfa 'W' => array ( 'Whoops\\' => 7, + 'WebSocket\\' => 10, ), 'T' => array ( @@ -134,11 +135,18 @@ class ComposerStaticInitb2555e5ff7197b9e020da74bbd3b7cfa 'Psr\\Container\\' => 14, 'Psr\\Clock\\' => 10, 'Psr\\Cache\\' => 10, + 'Phrity\\Util\\Transformer\\' => 24, + 'Phrity\\Util\\Interpolator\\' => 25, + 'Phrity\\Util\\' => 12, + 'Phrity\\Net\\' => 11, + 'Phrity\\Http\\' => 12, + 'Phrity\\Comparison\\' => 18, 'PhpParser\\' => 10, 'PhpOption\\' => 10, ), 'N' => array ( + 'Nyholm\\Psr7\\' => 12, 'NunoMaduro\\Collision\\' => 21, 'Nette\\' => 6, ), @@ -247,6 +255,10 @@ class ComposerStaticInitb2555e5ff7197b9e020da74bbd3b7cfa array ( 0 => __DIR__ . '/..' . '/filp/whoops/src/Whoops', ), + 'WebSocket\\' => + array ( + 0 => __DIR__ . '/..' . '/phrity/websocket/src', + ), 'TijsVerkoyen\\CssToInlineStyles\\' => array ( 0 => __DIR__ . '/..' . '/tijsverkoyen/css-to-inline-styles/src', @@ -478,6 +490,32 @@ class ComposerStaticInitb2555e5ff7197b9e020da74bbd3b7cfa array ( 0 => __DIR__ . '/..' . '/psr/cache/src', ), + 'Phrity\\Util\\Transformer\\' => + array ( + 0 => __DIR__ . '/..' . '/phrity/util-transformer/src', + ), + 'Phrity\\Util\\Interpolator\\' => + array ( + 0 => __DIR__ . '/..' . '/phrity/util-interpolator/src', + ), + 'Phrity\\Util\\' => + array ( + 0 => __DIR__ . '/..' . '/phrity/util-errorhandler/src', + 1 => __DIR__ . '/..' . '/phrity/util-accessor/src', + ), + 'Phrity\\Net\\' => + array ( + 0 => __DIR__ . '/..' . '/phrity/net-uri/src', + 1 => __DIR__ . '/..' . '/phrity/net-stream/src', + ), + 'Phrity\\Http\\' => + array ( + 0 => __DIR__ . '/..' . '/phrity/http/src', + ), + 'Phrity\\Comparison\\' => + array ( + 0 => __DIR__ . '/..' . '/phrity/comparison/src', + ), 'PhpParser\\' => array ( 0 => __DIR__ . '/..' . '/nikic/php-parser/lib/PhpParser', @@ -486,6 +524,10 @@ class ComposerStaticInitb2555e5ff7197b9e020da74bbd3b7cfa array ( 0 => __DIR__ . '/..' . '/phpoption/phpoption/src/PhpOption', ), + 'Nyholm\\Psr7\\' => + array ( + 0 => __DIR__ . '/..' . '/nyholm/psr7/src', + ), 'NunoMaduro\\Collision\\' => array ( 0 => __DIR__ . '/..' . '/nunomaduro/collision/src', @@ -755,6 +797,7 @@ class ComposerStaticInitb2555e5ff7197b9e020da74bbd3b7cfa 'App\\Console\\Commands\\RegisterApp' => __DIR__ . '/../..' . '/app/Console/Commands/RegisterApp.php', 'App\\EnhancedApps' => __DIR__ . '/../..' . '/app/EnhancedApps.php', 'App\\Facades\\Form' => __DIR__ . '/../..' . '/app/Facades/Form.php', + 'App\\Helpers\\TrueNASWebSocketClient' => __DIR__ . '/../..' . '/app/Helpers/TrueNASWebSocketClient.php', 'App\\Http\\Controllers\\Auth\\ForgotPasswordController' => __DIR__ . '/../..' . '/app/Http/Controllers/Auth/ForgotPasswordController.php', 'App\\Http\\Controllers\\Auth\\LoginController' => __DIR__ . '/../..' . '/app/Http/Controllers/Auth/LoginController.php', 'App\\Http\\Controllers\\Auth\\RegisterController' => __DIR__ . '/../..' . '/app/Http/Controllers/Auth/RegisterController.php', @@ -5921,6 +5964,17 @@ class ComposerStaticInitb2555e5ff7197b9e020da74bbd3b7cfa 'NunoMaduro\\Collision\\Provider' => __DIR__ . '/..' . '/nunomaduro/collision/src/Provider.php', 'NunoMaduro\\Collision\\SolutionsRepositories\\NullSolutionsRepository' => __DIR__ . '/..' . '/nunomaduro/collision/src/SolutionsRepositories/NullSolutionsRepository.php', 'NunoMaduro\\Collision\\Writer' => __DIR__ . '/..' . '/nunomaduro/collision/src/Writer.php', + 'Nyholm\\Psr7\\Factory\\HttplugFactory' => __DIR__ . '/..' . '/nyholm/psr7/src/Factory/HttplugFactory.php', + 'Nyholm\\Psr7\\Factory\\Psr17Factory' => __DIR__ . '/..' . '/nyholm/psr7/src/Factory/Psr17Factory.php', + 'Nyholm\\Psr7\\MessageTrait' => __DIR__ . '/..' . '/nyholm/psr7/src/MessageTrait.php', + 'Nyholm\\Psr7\\Request' => __DIR__ . '/..' . '/nyholm/psr7/src/Request.php', + 'Nyholm\\Psr7\\RequestTrait' => __DIR__ . '/..' . '/nyholm/psr7/src/RequestTrait.php', + 'Nyholm\\Psr7\\Response' => __DIR__ . '/..' . '/nyholm/psr7/src/Response.php', + 'Nyholm\\Psr7\\ServerRequest' => __DIR__ . '/..' . '/nyholm/psr7/src/ServerRequest.php', + 'Nyholm\\Psr7\\Stream' => __DIR__ . '/..' . '/nyholm/psr7/src/Stream.php', + 'Nyholm\\Psr7\\StreamTrait' => __DIR__ . '/..' . '/nyholm/psr7/src/StreamTrait.php', + 'Nyholm\\Psr7\\UploadedFile' => __DIR__ . '/..' . '/nyholm/psr7/src/UploadedFile.php', + 'Nyholm\\Psr7\\Uri' => __DIR__ . '/..' . '/nyholm/psr7/src/Uri.php', 'PHPUnit\\Event\\Application\\Finished' => __DIR__ . '/..' . '/phpunit/phpunit/src/Event/Events/Application/Finished.php', 'PHPUnit\\Event\\Application\\FinishedSubscriber' => __DIR__ . '/..' . '/phpunit/phpunit/src/Event/Events/Application/FinishedSubscriber.php', 'PHPUnit\\Event\\Application\\Started' => __DIR__ . '/..' . '/phpunit/phpunit/src/Event/Events/Application/Started.php', @@ -7275,6 +7329,51 @@ class ComposerStaticInitb2555e5ff7197b9e020da74bbd3b7cfa 'PhpParser\\PrettyPrinter\\Standard' => __DIR__ . '/..' . '/nikic/php-parser/lib/PhpParser/PrettyPrinter/Standard.php', 'PhpParser\\Token' => __DIR__ . '/..' . '/nikic/php-parser/lib/PhpParser/Token.php', 'PhpToken' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php', + 'Phrity\\Comparison\\Comparable' => __DIR__ . '/..' . '/phrity/comparison/src/Comparable.php', + 'Phrity\\Comparison\\Comparator' => __DIR__ . '/..' . '/phrity/comparison/src/Comparator.php', + 'Phrity\\Comparison\\ComparisonTrait' => __DIR__ . '/..' . '/phrity/comparison/src/ComparisonTrait.php', + 'Phrity\\Comparison\\Equalable' => __DIR__ . '/..' . '/phrity/comparison/src/Equalable.php', + 'Phrity\\Comparison\\IncomparableException' => __DIR__ . '/..' . '/phrity/comparison/src/IncomparableException.php', + 'Phrity\\Http\\HttpFactory' => __DIR__ . '/..' . '/phrity/http/src/HttpFactory.php', + 'Phrity\\Http\\Serializer' => __DIR__ . '/..' . '/phrity/http/src/Serializer.php', + 'Phrity\\Net\\Context' => __DIR__ . '/..' . '/phrity/net-stream/src/Context.php', + 'Phrity\\Net\\SocketClient' => __DIR__ . '/..' . '/phrity/net-stream/src/SocketClient.php', + 'Phrity\\Net\\SocketServer' => __DIR__ . '/..' . '/phrity/net-stream/src/SocketServer.php', + 'Phrity\\Net\\SocketStream' => __DIR__ . '/..' . '/phrity/net-stream/src/SocketStream.php', + 'Phrity\\Net\\Stream' => __DIR__ . '/..' . '/phrity/net-stream/src/Stream.php', + 'Phrity\\Net\\StreamCollection' => __DIR__ . '/..' . '/phrity/net-stream/src/StreamCollection.php', + 'Phrity\\Net\\StreamContainerInterface' => __DIR__ . '/..' . '/phrity/net-stream/src/StreamContainerInterface.php', + 'Phrity\\Net\\StreamException' => __DIR__ . '/..' . '/phrity/net-stream/src/StreamException.php', + 'Phrity\\Net\\StreamFactory' => __DIR__ . '/..' . '/phrity/net-stream/src/StreamFactory.php', + 'Phrity\\Net\\StreamInterface' => __DIR__ . '/..' . '/phrity/net-stream/src/StreamInterface.php', + 'Phrity\\Net\\Uri' => __DIR__ . '/..' . '/phrity/net-uri/src/Uri.php', + 'Phrity\\Net\\UriFactory' => __DIR__ . '/..' . '/phrity/net-uri/src/UriFactory.php', + 'Phrity\\Util\\Accessor' => __DIR__ . '/..' . '/phrity/util-accessor/src/Accessor.php', + 'Phrity\\Util\\AccessorException' => __DIR__ . '/..' . '/phrity/util-accessor/src/AccessorException.php', + 'Phrity\\Util\\AccessorTrait' => __DIR__ . '/..' . '/phrity/util-accessor/src/AccessorTrait.php', + 'Phrity\\Util\\DataAccessor' => __DIR__ . '/..' . '/phrity/util-accessor/src/DataAccessor.php', + 'Phrity\\Util\\ErrorHandler' => __DIR__ . '/..' . '/phrity/util-errorhandler/src/ErrorHandler.php', + 'Phrity\\Util\\Interpolator\\Interpolator' => __DIR__ . '/..' . '/phrity/util-interpolator/src/Interpolator.php', + 'Phrity\\Util\\Interpolator\\InterpolatorTrait' => __DIR__ . '/..' . '/phrity/util-interpolator/src/InterpolatorTrait.php', + 'Phrity\\Util\\PathAccessor' => __DIR__ . '/..' . '/phrity/util-accessor/src/PathAccessor.php', + 'Phrity\\Util\\Transformer\\BasicTypeConverter' => __DIR__ . '/..' . '/phrity/util-transformer/src/BasicTypeConverter.php', + 'Phrity\\Util\\Transformer\\ChainedResolver' => __DIR__ . '/..' . '/phrity/util-transformer/src/ChainedResolver.php', + 'Phrity\\Util\\Transformer\\DateTimeConverter' => __DIR__ . '/..' . '/phrity/util-transformer/src/DateTimeConverter.php', + 'Phrity\\Util\\Transformer\\EnumConverter' => __DIR__ . '/..' . '/phrity/util-transformer/src/EnumConverter.php', + 'Phrity\\Util\\Transformer\\FirstMatchResolver' => __DIR__ . '/..' . '/phrity/util-transformer/src/FirstMatchResolver.php', + 'Phrity\\Util\\Transformer\\FlattenDecoder' => __DIR__ . '/..' . '/phrity/util-transformer/src/FlattenDecoder.php', + 'Phrity\\Util\\Transformer\\JsonDecoder' => __DIR__ . '/..' . '/phrity/util-transformer/src/JsonDecoder.php', + 'Phrity\\Util\\Transformer\\JsonSerializableConverter' => __DIR__ . '/..' . '/phrity/util-transformer/src/JsonSerializableConverter.php', + 'Phrity\\Util\\Transformer\\ReadableConverter' => __DIR__ . '/..' . '/phrity/util-transformer/src/ReadableConverter.php', + 'Phrity\\Util\\Transformer\\RecursionResolver' => __DIR__ . '/..' . '/phrity/util-transformer/src/RecursionResolver.php', + 'Phrity\\Util\\Transformer\\ReversedReadableConverter' => __DIR__ . '/..' . '/phrity/util-transformer/src/ReversedReadableConverter.php', + 'Phrity\\Util\\Transformer\\StringResolver' => __DIR__ . '/..' . '/phrity/util-transformer/src/StringResolver.php', + 'Phrity\\Util\\Transformer\\StringableConverter' => __DIR__ . '/..' . '/phrity/util-transformer/src/StringableConverter.php', + 'Phrity\\Util\\Transformer\\Symfony\\NormalizerWrapper' => __DIR__ . '/..' . '/phrity/util-transformer/src/Symfony/NormalizerWrapper.php', + 'Phrity\\Util\\Transformer\\ThrowableConverter' => __DIR__ . '/..' . '/phrity/util-transformer/src/ThrowableConverter.php', + 'Phrity\\Util\\Transformer\\TransformerException' => __DIR__ . '/..' . '/phrity/util-transformer/src/TransformerException.php', + 'Phrity\\Util\\Transformer\\TransformerInterface' => __DIR__ . '/..' . '/phrity/util-transformer/src/TransformerInterface.php', + 'Phrity\\Util\\Transformer\\Type' => __DIR__ . '/..' . '/phrity/util-transformer/src/Type.php', 'Psr\\Cache\\CacheException' => __DIR__ . '/..' . '/psr/cache/src/CacheException.php', 'Psr\\Cache\\CacheItemInterface' => __DIR__ . '/..' . '/psr/cache/src/CacheItemInterface.php', 'Psr\\Cache\\CacheItemPoolInterface' => __DIR__ . '/..' . '/psr/cache/src/CacheItemPoolInterface.php', @@ -9573,6 +9672,8 @@ class ComposerStaticInitb2555e5ff7197b9e020da74bbd3b7cfa 'Termwind\\ValueObjects\\Node' => __DIR__ . '/..' . '/nunomaduro/termwind/src/ValueObjects/Node.php', 'Termwind\\ValueObjects\\Style' => __DIR__ . '/..' . '/nunomaduro/termwind/src/ValueObjects/Style.php', 'Termwind\\ValueObjects\\Styles' => __DIR__ . '/..' . '/nunomaduro/termwind/src/ValueObjects/Styles.php', + 'Tests\\Feature\\AjaxPostEndpointsTest' => __DIR__ . '/../..' . '/tests/Feature/AjaxPostEndpointsTest.php', + 'Tests\\Feature\\CsrfExceptionsTest' => __DIR__ . '/../..' . '/tests/Feature/CsrfExceptionsTest.php', 'Tests\\Feature\\DashTest' => __DIR__ . '/../..' . '/tests/Feature/DashTest.php', 'Tests\\Feature\\GetStatsTest' => __DIR__ . '/../..' . '/tests/Feature/GetStatsTest.php', 'Tests\\Feature\\ImportTest' => __DIR__ . '/../..' . '/tests/Feature/ImportTest.php', @@ -9581,16 +9682,24 @@ class ComposerStaticInitb2555e5ff7197b9e020da74bbd3b7cfa 'Tests\\Feature\\ItemExportTest' => __DIR__ . '/../..' . '/tests/Feature/ItemExportTest.php', 'Tests\\Feature\\ItemImportTest' => __DIR__ . '/../..' . '/tests/Feature/ItemImportTest.php', 'Tests\\Feature\\ItemListTest' => __DIR__ . '/../..' . '/tests/Feature/ItemListTest.php', + 'Tests\\Feature\\ItemOwnershipTest' => __DIR__ . '/../..' . '/tests/Feature/ItemOwnershipTest.php', + 'Tests\\Feature\\OrphanItemRecoveryTest' => __DIR__ . '/../..' . '/tests/Feature/OrphanItemRecoveryTest.php', + 'Tests\\Feature\\RoutesRenderTest' => __DIR__ . '/../..' . '/tests/Feature/RoutesRenderTest.php', 'Tests\\Feature\\SearchTest' => __DIR__ . '/../..' . '/tests/Feature/SearchTest.php', 'Tests\\Feature\\SettingsTest' => __DIR__ . '/../..' . '/tests/Feature/SettingsTest.php', + 'Tests\\Feature\\StorageDiskTest' => __DIR__ . '/../..' . '/tests/Feature/StorageDiskTest.php', 'Tests\\Feature\\TagListTest' => __DIR__ . '/../..' . '/tests/Feature/TagListTest.php', 'Tests\\Feature\\TrustHostsTest' => __DIR__ . '/../..' . '/tests/Feature/TrustHostsTest.php', 'Tests\\Feature\\TrustProxiesTest' => __DIR__ . '/../..' . '/tests/Feature/TrustProxiesTest.php', + 'Tests\\Feature\\UserDeleteItemsTest' => __DIR__ . '/../..' . '/tests/Feature/UserDeleteItemsTest.php', 'Tests\\Feature\\UserEditTest' => __DIR__ . '/../..' . '/tests/Feature/UserEditTest.php', 'Tests\\Feature\\UserListTest' => __DIR__ . '/../..' . '/tests/Feature/UserListTest.php', 'Tests\\TestCase' => __DIR__ . '/../..' . '/tests/TestCase.php', 'Tests\\Unit\\database\\seeders\\SettingsSeederTest' => __DIR__ . '/../..' . '/tests/Unit/database/seeders/SettingsSeederTest.php', + 'Tests\\Unit\\helpers\\ClassNameTest' => __DIR__ . '/../..' . '/tests/Unit/helpers/ClassNameTest.php', + 'Tests\\Unit\\helpers\\ColorHelpersTest' => __DIR__ . '/../..' . '/tests/Unit/helpers/ColorHelpersTest.php', 'Tests\\Unit\\helpers\\IsImageTest' => __DIR__ . '/../..' . '/tests/Unit/helpers/IsImageTest.php', + 'Tests\\Unit\\helpers\\SizeHelpersTest' => __DIR__ . '/../..' . '/tests/Unit/helpers/SizeHelpersTest.php', 'Tests\\Unit\\helpers\\SlugTest' => __DIR__ . '/../..' . '/tests/Unit/helpers/SlugTest.php', 'Tests\\Unit\\lang\\LangTest' => __DIR__ . '/../..' . '/tests/Unit/lang/LangTest.php', 'TheSeer\\Tokenizer\\Exception' => __DIR__ . '/..' . '/theseer/tokenizer/src/Exception.php', @@ -9609,6 +9718,66 @@ class ComposerStaticInitb2555e5ff7197b9e020da74bbd3b7cfa 'TijsVerkoyen\\CssToInlineStyles\\Css\\Rule\\Rule' => __DIR__ . '/..' . '/tijsverkoyen/css-to-inline-styles/src/Css/Rule/Rule.php', 'UnhandledMatchError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php', 'ValueError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/ValueError.php', + 'WebSocket\\Client' => __DIR__ . '/..' . '/phrity/websocket/src/Client.php', + 'WebSocket\\Configuration' => __DIR__ . '/..' . '/phrity/websocket/src/Configuration.php', + 'WebSocket\\Connection' => __DIR__ . '/..' . '/phrity/websocket/src/Connection.php', + 'WebSocket\\Constant' => __DIR__ . '/..' . '/phrity/websocket/src/Constant.php', + 'WebSocket\\Exception\\BadOpcodeException' => __DIR__ . '/..' . '/phrity/websocket/src/Exception/BadOpcodeException.php', + 'WebSocket\\Exception\\BadUriException' => __DIR__ . '/..' . '/phrity/websocket/src/Exception/BadUriException.php', + 'WebSocket\\Exception\\ClientException' => __DIR__ . '/..' . '/phrity/websocket/src/Exception/ClientException.php', + 'WebSocket\\Exception\\CloseException' => __DIR__ . '/..' . '/phrity/websocket/src/Exception/CloseException.php', + 'WebSocket\\Exception\\ConnectionClosedException' => __DIR__ . '/..' . '/phrity/websocket/src/Exception/ConnectionClosedException.php', + 'WebSocket\\Exception\\ConnectionFailureException' => __DIR__ . '/..' . '/phrity/websocket/src/Exception/ConnectionFailureException.php', + 'WebSocket\\Exception\\ConnectionLevelInterface' => __DIR__ . '/..' . '/phrity/websocket/src/Exception/ConnectionLevelInterface.php', + 'WebSocket\\Exception\\ConnectionTimeoutException' => __DIR__ . '/..' . '/phrity/websocket/src/Exception/ConnectionTimeoutException.php', + 'WebSocket\\Exception\\Exception' => __DIR__ . '/..' . '/phrity/websocket/src/Exception/Exception.php', + 'WebSocket\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/phrity/websocket/src/Exception/ExceptionInterface.php', + 'WebSocket\\Exception\\HandshakeException' => __DIR__ . '/..' . '/phrity/websocket/src/Exception/HandshakeException.php', + 'WebSocket\\Exception\\MessageLevelInterface' => __DIR__ . '/..' . '/phrity/websocket/src/Exception/MessageLevelInterface.php', + 'WebSocket\\Exception\\ReconnectException' => __DIR__ . '/..' . '/phrity/websocket/src/Exception/ReconnectException.php', + 'WebSocket\\Exception\\RunnerException' => __DIR__ . '/..' . '/phrity/websocket/src/Exception/RunnerException.php', + 'WebSocket\\Exception\\ServerException' => __DIR__ . '/..' . '/phrity/websocket/src/Exception/ServerException.php', + 'WebSocket\\Frame\\Frame' => __DIR__ . '/..' . '/phrity/websocket/src/Frame/Frame.php', + 'WebSocket\\Frame\\FrameHandler' => __DIR__ . '/..' . '/phrity/websocket/src/Frame/FrameHandler.php', + 'WebSocket\\Http\\DefaultHttpFactory' => __DIR__ . '/..' . '/phrity/websocket/src/Http/DefaultHttpFactory.php', + 'WebSocket\\Http\\HttpHandler' => __DIR__ . '/..' . '/phrity/websocket/src/Http/HttpHandler.php', + 'WebSocket\\Http\\Request' => __DIR__ . '/..' . '/phrity/websocket/src/Http/Request.php', + 'WebSocket\\Http\\Response' => __DIR__ . '/..' . '/phrity/websocket/src/Http/Response.php', + 'WebSocket\\Http\\ServerRequest' => __DIR__ . '/..' . '/phrity/websocket/src/Http/ServerRequest.php', + 'WebSocket\\Message\\Binary' => __DIR__ . '/..' . '/phrity/websocket/src/Message/Binary.php', + 'WebSocket\\Message\\Close' => __DIR__ . '/..' . '/phrity/websocket/src/Message/Close.php', + 'WebSocket\\Message\\Message' => __DIR__ . '/..' . '/phrity/websocket/src/Message/Message.php', + 'WebSocket\\Message\\MessageHandler' => __DIR__ . '/..' . '/phrity/websocket/src/Message/MessageHandler.php', + 'WebSocket\\Message\\Ping' => __DIR__ . '/..' . '/phrity/websocket/src/Message/Ping.php', + 'WebSocket\\Message\\Pong' => __DIR__ . '/..' . '/phrity/websocket/src/Message/Pong.php', + 'WebSocket\\Message\\Text' => __DIR__ . '/..' . '/phrity/websocket/src/Message/Text.php', + 'WebSocket\\Middleware\\Callback' => __DIR__ . '/..' . '/phrity/websocket/src/Middleware/Callback.php', + 'WebSocket\\Middleware\\CloseHandler' => __DIR__ . '/..' . '/phrity/websocket/src/Middleware/CloseHandler.php', + 'WebSocket\\Middleware\\CompressionExtension' => __DIR__ . '/..' . '/phrity/websocket/src/Middleware/CompressionExtension.php', + 'WebSocket\\Middleware\\CompressionExtension\\CompressorInterface' => __DIR__ . '/..' . '/phrity/websocket/src/Middleware/CompressionExtension/CompressorInterface.php', + 'WebSocket\\Middleware\\CompressionExtension\\DeflateCompressor' => __DIR__ . '/..' . '/phrity/websocket/src/Middleware/CompressionExtension/DeflateCompressor.php', + 'WebSocket\\Middleware\\FollowRedirect' => __DIR__ . '/..' . '/phrity/websocket/src/Middleware/FollowRedirect.php', + 'WebSocket\\Middleware\\MiddlewareHandler' => __DIR__ . '/..' . '/phrity/websocket/src/Middleware/MiddlewareHandler.php', + 'WebSocket\\Middleware\\MiddlewareInterface' => __DIR__ . '/..' . '/phrity/websocket/src/Middleware/MiddlewareInterface.php', + 'WebSocket\\Middleware\\PingInterval' => __DIR__ . '/..' . '/phrity/websocket/src/Middleware/PingInterval.php', + 'WebSocket\\Middleware\\PingResponder' => __DIR__ . '/..' . '/phrity/websocket/src/Middleware/PingResponder.php', + 'WebSocket\\Middleware\\ProcessHttpIncomingInterface' => __DIR__ . '/..' . '/phrity/websocket/src/Middleware/ProcessHttpIncomingInterface.php', + 'WebSocket\\Middleware\\ProcessHttpOutgoingInterface' => __DIR__ . '/..' . '/phrity/websocket/src/Middleware/ProcessHttpOutgoingInterface.php', + 'WebSocket\\Middleware\\ProcessHttpStack' => __DIR__ . '/..' . '/phrity/websocket/src/Middleware/ProcessHttpStack.php', + 'WebSocket\\Middleware\\ProcessIncomingInterface' => __DIR__ . '/..' . '/phrity/websocket/src/Middleware/ProcessIncomingInterface.php', + 'WebSocket\\Middleware\\ProcessOutgoingInterface' => __DIR__ . '/..' . '/phrity/websocket/src/Middleware/ProcessOutgoingInterface.php', + 'WebSocket\\Middleware\\ProcessStack' => __DIR__ . '/..' . '/phrity/websocket/src/Middleware/ProcessStack.php', + 'WebSocket\\Middleware\\ProcessTickInterface' => __DIR__ . '/..' . '/phrity/websocket/src/Middleware/ProcessTickInterface.php', + 'WebSocket\\Middleware\\ProcessTickStack' => __DIR__ . '/..' . '/phrity/websocket/src/Middleware/ProcessTickStack.php', + 'WebSocket\\Middleware\\SubprotocolNegotiation' => __DIR__ . '/..' . '/phrity/websocket/src/Middleware/SubprotocolNegotiation.php', + 'WebSocket\\Runtime\\IdentityInterface' => __DIR__ . '/..' . '/phrity/websocket/src/Runtime/IdentityInterface.php', + 'WebSocket\\Runtime\\Runner' => __DIR__ . '/..' . '/phrity/websocket/src/Runtime/Runner.php', + 'WebSocket\\Server' => __DIR__ . '/..' . '/phrity/websocket/src/Server.php', + 'WebSocket\\Trait\\ConfigurationTrait' => __DIR__ . '/..' . '/phrity/websocket/src/Trait/ConfigurationTrait.php', + 'WebSocket\\Trait\\ListenerTrait' => __DIR__ . '/..' . '/phrity/websocket/src/Trait/ListenerTrait.php', + 'WebSocket\\Trait\\OpcodeTrait' => __DIR__ . '/..' . '/phrity/websocket/src/Trait/OpcodeTrait.php', + 'WebSocket\\Trait\\SendMethodsTrait' => __DIR__ . '/..' . '/phrity/websocket/src/Trait/SendMethodsTrait.php', + 'WebSocket\\Trait\\StringableTrait' => __DIR__ . '/..' . '/phrity/websocket/src/Trait/StringableTrait.php', 'Whoops\\Exception\\ErrorException' => __DIR__ . '/..' . '/filp/whoops/src/Whoops/Exception/ErrorException.php', 'Whoops\\Exception\\Formatter' => __DIR__ . '/..' . '/filp/whoops/src/Whoops/Exception/Formatter.php', 'Whoops\\Exception\\Frame' => __DIR__ . '/..' . '/filp/whoops/src/Whoops/Exception/Frame.php', diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json index 79e537e14..5e5890780 100644 --- a/vendor/composer/installed.json +++ b/vendor/composer/installed.json @@ -4235,6 +4235,87 @@ ], "install-path": "../nunomaduro/termwind" }, + { + "name": "nyholm/psr7", + "version": "1.8.2", + "version_normalized": "1.8.2.0", + "source": { + "type": "git", + "url": "https://github.com/Nyholm/psr7.git", + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Nyholm/psr7/zipball/a71f2b11690f4b24d099d6b16690a90ae14fc6f3", + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0" + }, + "provide": { + "php-http/message-factory-implementation": "1.0", + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "http-interop/http-factory-tests": "^0.9", + "php-http/message-factory": "^1.0", + "php-http/psr7-integration-tests": "^1.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.4", + "symfony/error-handler": "^4.4" + }, + "time": "2024-09-09T07:06:30+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.8-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Nyholm\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + }, + { + "name": "Martijn van der Ven", + "email": "martijn@vanderven.se" + } + ], + "description": "A fast PHP7 implementation of PSR-7", + "homepage": "https://tnyholm.se", + "keywords": [ + "psr-17", + "psr-7" + ], + "support": { + "issues": "https://github.com/Nyholm/psr7/issues", + "source": "https://github.com/Nyholm/psr7/tree/1.8.2" + }, + "funding": [ + { + "url": "https://github.com/Zegnat", + "type": "github" + }, + { + "url": "https://github.com/nyholm", + "type": "github" + } + ], + "install-path": "../nyholm/psr7" + }, { "name": "phar-io/manifest", "version": "2.0.4", @@ -5346,6 +5427,553 @@ ], "install-path": "../phpunit/phpunit" }, + { + "name": "phrity/comparison", + "version": "1.4.1", + "version_normalized": "1.4.1.0", + "source": { + "type": "git", + "url": "https://github.com/sirn-se/phrity-comparison.git", + "reference": "cf80abb822537eeaaeb4142157cd667ca6372a13" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sirn-se/phrity-comparison/zipball/cf80abb822537eeaaeb4142157cd667ca6372a13", + "reference": "cf80abb822537eeaaeb4142157cd667ca6372a13", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", + "robiningelbrecht/phpunit-coverage-tools": "^1.9", + "squizlabs/php_codesniffer": "^3.5 || ^4.0" + }, + "time": "2025-12-05T07:38:30+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Phrity\\Comparison\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sören Jensen", + "email": "sirn@sirn.se", + "homepage": "https://phrity.sirn.se" + } + ], + "description": "Interfaces and helper trait for comparing objects. Comparator for sort and filter applications.", + "homepage": "https://phrity.sirn.se/comparison", + "keywords": [ + "comparable", + "comparator", + "comparison", + "equalable", + "filter", + "sort" + ], + "support": { + "issues": "https://github.com/sirn-se/phrity-comparison/issues", + "source": "https://github.com/sirn-se/phrity-comparison/tree/1.4.1" + }, + "install-path": "../phrity/comparison" + }, + { + "name": "phrity/http", + "version": "1.1.0", + "version_normalized": "1.1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sirn-se/phrity-http.git", + "reference": "1e7eee67359287b94aae2b7d40b730d5f5394943" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sirn-se/phrity-http/zipball/1e7eee67359287b94aae2b7d40b730d5f5394943", + "reference": "1e7eee67359287b94aae2b7d40b730d5f5394943", + "shasum": "" + }, + "require": { + "php": "^8.1", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0" + }, + "require-dev": { + "guzzlehttp/psr7": "^2.0", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", + "robiningelbrecht/phpunit-coverage-tools": "^1.9", + "squizlabs/php_codesniffer": "^3.5 || ^4.0" + }, + "time": "2025-12-22T20:22:29+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Phrity\\Http\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sören Jensen", + "email": "sirn@sirn.se", + "homepage": "https://phrity.sirn.se" + } + ], + "description": "Utilities and interfaces for handling HTTP.", + "homepage": "https://phrity.sirn.se/http", + "keywords": [ + "HTTP Factories", + "HTTP Serializer", + "http", + "psr-17", + "psr-7" + ], + "support": { + "issues": "https://github.com/sirn-se/phrity-http/issues", + "source": "https://github.com/sirn-se/phrity-http/tree/1.1.0" + }, + "install-path": "../phrity/http" + }, + { + "name": "phrity/net-stream", + "version": "2.4.0", + "version_normalized": "2.4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sirn-se/phrity-net-stream.git", + "reference": "a0726a8dddf11a4cd3a9e45d17e145d78e948f43" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sirn-se/phrity-net-stream/zipball/a0726a8dddf11a4cd3a9e45d17e145d78e948f43", + "reference": "a0726a8dddf11a4cd3a9e45d17e145d78e948f43", + "shasum": "" + }, + "require": { + "php": "^8.1", + "phrity/util-errorhandler": "^1.1", + "phrity/util-interpolator": "^1.1", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0 || ^13.0", + "phrity/net-uri": "^2.0", + "robiningelbrecht/phpunit-coverage-tools": "^1.9", + "squizlabs/php_codesniffer": "^4.0" + }, + "time": "2026-03-25T18:16:40+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": "Socket stream classes implementing PSR-7 Stream and PSR-17 StreamFactory", + "homepage": "https://phrity.sirn.se/net-stream", + "keywords": [ + "Socket", + "client", + "psr-17", + "psr-7", + "server", + "stream", + "stream factory" + ], + "support": { + "issues": "https://github.com/sirn-se/phrity-net-stream/issues", + "source": "https://github.com/sirn-se/phrity-net-stream/tree/2.4.0" + }, + "install-path": "../phrity/net-stream" + }, + { + "name": "phrity/net-uri", + "version": "2.2.1", + "version_normalized": "2.2.1.0", + "source": { + "type": "git", + "url": "https://github.com/sirn-se/phrity-net-uri.git", + "reference": "0737de026b75177ae302ac9fdbbd0ffc2610f3b8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sirn-se/phrity-net-uri/zipball/0737de026b75177ae302ac9fdbbd0ffc2610f3b8", + "reference": "0737de026b75177ae302ac9fdbbd0ffc2610f3b8", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^8.1", + "phrity/comparison": "^1.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 | ^2.0" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", + "phrity/util-errorhandler": "^1.1", + "robiningelbrecht/phpunit-coverage-tools": "^1.9", + "squizlabs/php_codesniffer": "^3.5 || ^4.0" + }, + "suggest": { + "ext-intl": "Enables IDN conversion for non-ASCII domains" + }, + "time": "2025-12-05T10:39:22+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/2.2.1" + }, + "install-path": "../phrity/net-uri" + }, + { + "name": "phrity/util-accessor", + "version": "1.3.1", + "version_normalized": "1.3.1.0", + "source": { + "type": "git", + "url": "https://github.com/sirn-se/phrity-util-accessor.git", + "reference": "e9741da6b532fa2da7f627fce60fa77f00f6e354" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sirn-se/phrity-util-accessor/zipball/e9741da6b532fa2da7f627fce60fa77f00f6e354", + "reference": "e9741da6b532fa2da7f627fce60fa77f00f6e354", + "shasum": "" + }, + "require": { + "php": "^8.1", + "phrity/util-transformer": "^1.0" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", + "robiningelbrecht/phpunit-coverage-tools": "^1.9", + "squizlabs/php_codesniffer": "^3.5 || ^4.0" + }, + "time": "2025-12-05T21:18:15+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": "Utility to handle access to a data set by using access paths. Similar to and partly compatible with xpath and json-pointer.", + "homepage": "https://phrity.sirn.se/util-accessor", + "keywords": [ + "accessor" + ], + "support": { + "issues": "https://github.com/sirn-se/phrity-util-accessor/issues", + "source": "https://github.com/sirn-se/phrity-util-accessor/tree/1.3.1" + }, + "install-path": "../phrity/util-accessor" + }, + { + "name": "phrity/util-errorhandler", + "version": "1.2.2", + "version_normalized": "1.2.2.0", + "source": { + "type": "git", + "url": "https://github.com/sirn-se/phrity-util-errorhandler.git", + "reference": "70a669cc22db2eed6a109ec66fd95168a4332c9b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sirn-se/phrity-util-errorhandler/zipball/70a669cc22db2eed6a109ec66fd95168a4332c9b", + "reference": "70a669cc22db2eed6a109ec66fd95168a4332c9b", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", + "robiningelbrecht/phpunit-coverage-tools": "^1.9", + "squizlabs/php_codesniffer": "^3.5 || ^4.0" + }, + "time": "2025-12-05T21:25:36+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.2.2" + }, + "install-path": "../phrity/util-errorhandler" + }, + { + "name": "phrity/util-interpolator", + "version": "1.1.1", + "version_normalized": "1.1.1.0", + "source": { + "type": "git", + "url": "https://github.com/sirn-se/phrity-util-interpolator.git", + "reference": "18b0362d2aff60984c530808333c5e64c80c3e50" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sirn-se/phrity-util-interpolator/zipball/18b0362d2aff60984c530808333c5e64c80c3e50", + "reference": "18b0362d2aff60984c530808333c5e64c80c3e50", + "shasum": "" + }, + "require": { + "php": "^8.1", + "phrity/util-accessor": "^1.3", + "phrity/util-transformer": "^1.3" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", + "robiningelbrecht/phpunit-coverage-tools": "^1.9", + "squizlabs/php_codesniffer": "^3.5 || ^4.0" + }, + "time": "2025-12-05T21:35:25+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Phrity\\Util\\Interpolator\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sören Jensen", + "email": "sirn@sirn.se", + "homepage": "https://phrity.sirn.se" + } + ], + "description": "Interpolator class and trait.", + "homepage": "https://phrity.sirn.se/util-interpolator", + "keywords": [ + "interpolate", + "interpolator" + ], + "support": { + "issues": "https://github.com/sirn-se/phrity-util-interpolator/issues", + "source": "https://github.com/sirn-se/phrity-util-interpolator/tree/1.1.1" + }, + "install-path": "../phrity/util-interpolator" + }, + { + "name": "phrity/util-transformer", + "version": "1.3.1", + "version_normalized": "1.3.1.0", + "source": { + "type": "git", + "url": "https://github.com/sirn-se/phrity-util-transformer.git", + "reference": "3fd4d7fa4077deafe5130c4b9ee8146b5488abe3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sirn-se/phrity-util-transformer/zipball/3fd4d7fa4077deafe5130c4b9ee8146b5488abe3", + "reference": "3fd4d7fa4077deafe5130c4b9ee8146b5488abe3", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", + "robiningelbrecht/phpunit-coverage-tools": "^1.9", + "squizlabs/php_codesniffer": "^3.5 || ^4.0", + "symfony/property-access": "^6.0 || ^7.0 || ^8.0", + "symfony/serializer": "^6.0 || ^7.0 || ^8.0", + "symfony/translation-contracts": "^3.0", + "symfony/uid": "^6.0 || ^7.0 || ^8.0" + }, + "time": "2025-12-05T22:07:04+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Phrity\\Util\\Transformer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sören Jensen", + "email": "sirn@sirn.se", + "homepage": "https://phrity.sirn.se" + } + ], + "description": "Type transformers, normalizers and resolvers.", + "homepage": "https://phrity.sirn.se/util-transformer", + "keywords": [ + "normalizer", + "resolver", + "transformer", + "type" + ], + "support": { + "issues": "https://github.com/sirn-se/phrity-util-transformer/issues", + "source": "https://github.com/sirn-se/phrity-util-transformer/tree/1.3.1" + }, + "install-path": "../phrity/util-transformer" + }, + { + "name": "phrity/websocket", + "version": "3.7.3", + "version_normalized": "3.7.3.0", + "source": { + "type": "git", + "url": "https://github.com/sirn-se/websocket-php.git", + "reference": "c92d613ec298ea108b8ebae306609cd40427f7d6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sirn-se/websocket-php/zipball/c92d613ec298ea108b8ebae306609cd40427f7d6", + "reference": "c92d613ec298ea108b8ebae306609cd40427f7d6", + "shasum": "" + }, + "require": { + "nyholm/psr7": "^1.4", + "php": "^8.1", + "phrity/http": "^1.1", + "phrity/net-stream": "^2.4", + "phrity/net-uri": "^2.1", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "psr/log": "^1.0 || ^2.0 || ^3.0" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0 || ^13.0", + "phrity/logger-console": "^1.0", + "phrity/net-mock": "^2.4", + "phrity/util-errorhandler": "^1.1", + "robiningelbrecht/phpunit-coverage-tools": "^1.9", + "squizlabs/php_codesniffer": "^4.0" + }, + "time": "2026-06-22T14:53:45+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "WebSocket\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Fredrik Liljegren" + }, + { + "name": "Sören Jensen", + "email": "sirn@sirn.se", + "homepage": "https://phrity.sirn.se" + } + ], + "description": "WebSocket client and server", + "homepage": "https://phrity.sirn.se/websocket", + "keywords": [ + "client", + "server", + "websocket" + ], + "support": { + "issues": "https://github.com/sirn-se/websocket-php/issues", + "source": "https://github.com/sirn-se/websocket-php/tree/3.7.3" + }, + "install-path": "../phrity/websocket" + }, { "name": "psr/cache", "version": "3.0.0", diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php index ac2164c6b..7a8b25402 100644 --- a/vendor/composer/installed.php +++ b/vendor/composer/installed.php @@ -3,7 +3,7 @@ 'name' => 'laravel/laravel', 'pretty_version' => '2.x-dev', 'version' => '2.9999999.9999999.9999999-dev', - 'reference' => '39191885e53b2c8ab49b3d594ece9825b6fd87b4', + 'reference' => '93a321ff5b77e698e52d0e204487a968df4b5fc9', 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), @@ -508,7 +508,7 @@ 'laravel/laravel' => array( 'pretty_version' => '2.x-dev', 'version' => '2.9999999.9999999.9999999-dev', - 'reference' => '39191885e53b2c8ab49b3d594ece9825b6fd87b4', + 'reference' => '93a321ff5b77e698e52d0e204487a968df4b5fc9', 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), @@ -727,6 +727,15 @@ 'aliases' => array(), 'dev_requirement' => false, ), + 'nyholm/psr7' => array( + 'pretty_version' => '1.8.2', + 'version' => '1.8.2.0', + 'reference' => 'a71f2b11690f4b24d099d6b16690a90ae14fc6f3', + 'type' => 'library', + 'install_path' => __DIR__ . '/../nyholm/psr7', + 'aliases' => array(), + 'dev_requirement' => false, + ), 'phar-io/manifest' => array( 'pretty_version' => '2.0.4', 'version' => '2.0.4.0', @@ -889,6 +898,87 @@ 'aliases' => array(), 'dev_requirement' => true, ), + 'phrity/comparison' => array( + 'pretty_version' => '1.4.1', + 'version' => '1.4.1.0', + 'reference' => 'cf80abb822537eeaaeb4142157cd667ca6372a13', + 'type' => 'library', + 'install_path' => __DIR__ . '/../phrity/comparison', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'phrity/http' => array( + 'pretty_version' => '1.1.0', + 'version' => '1.1.0.0', + 'reference' => '1e7eee67359287b94aae2b7d40b730d5f5394943', + 'type' => 'library', + 'install_path' => __DIR__ . '/../phrity/http', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'phrity/net-stream' => array( + 'pretty_version' => '2.4.0', + 'version' => '2.4.0.0', + 'reference' => 'a0726a8dddf11a4cd3a9e45d17e145d78e948f43', + 'type' => 'library', + 'install_path' => __DIR__ . '/../phrity/net-stream', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'phrity/net-uri' => array( + 'pretty_version' => '2.2.1', + 'version' => '2.2.1.0', + 'reference' => '0737de026b75177ae302ac9fdbbd0ffc2610f3b8', + 'type' => 'library', + 'install_path' => __DIR__ . '/../phrity/net-uri', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'phrity/util-accessor' => array( + 'pretty_version' => '1.3.1', + 'version' => '1.3.1.0', + 'reference' => 'e9741da6b532fa2da7f627fce60fa77f00f6e354', + 'type' => 'library', + 'install_path' => __DIR__ . '/../phrity/util-accessor', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'phrity/util-errorhandler' => array( + 'pretty_version' => '1.2.2', + 'version' => '1.2.2.0', + 'reference' => '70a669cc22db2eed6a109ec66fd95168a4332c9b', + 'type' => 'library', + 'install_path' => __DIR__ . '/../phrity/util-errorhandler', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'phrity/util-interpolator' => array( + 'pretty_version' => '1.1.1', + 'version' => '1.1.1.0', + 'reference' => '18b0362d2aff60984c530808333c5e64c80c3e50', + 'type' => 'library', + 'install_path' => __DIR__ . '/../phrity/util-interpolator', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'phrity/util-transformer' => array( + 'pretty_version' => '1.3.1', + 'version' => '1.3.1.0', + 'reference' => '3fd4d7fa4077deafe5130c4b9ee8146b5488abe3', + 'type' => 'library', + 'install_path' => __DIR__ . '/../phrity/util-transformer', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'phrity/websocket' => array( + 'pretty_version' => '3.7.3', + 'version' => '3.7.3.0', + 'reference' => 'c92d613ec298ea108b8ebae306609cd40427f7d6', + 'type' => 'library', + 'install_path' => __DIR__ . '/../phrity/websocket', + 'aliases' => array(), + 'dev_requirement' => false, + ), 'psr/cache' => array( 'pretty_version' => '3.0.0', 'version' => '3.0.0.0', diff --git a/vendor/nyholm/psr7/CHANGELOG.md b/vendor/nyholm/psr7/CHANGELOG.md new file mode 100644 index 000000000..17a819fbb --- /dev/null +++ b/vendor/nyholm/psr7/CHANGELOG.md @@ -0,0 +1,172 @@ +# Changelog + +All notable changes to this project will be documented in this file, in reverse chronological order by release. + +## 1.8.2 + +- Fix deprecation warnings in PHP 8.4 + +## 1.8.1 + +- Fix error handling in Stream::getContents() + +## 1.8.0 + +- Deprecate HttplugFactory, use Psr17Factory instead +- Make depencendy on php-http/message-factory optional + +## 1.7.0 + +- Bump to PHP 7.2 minimum +- Allow psr/http-message v2 +- Use copy-on-write for streams created from strings + +## 1.6.1 + +- Security fix: CVE-2023-29197 + +## 1.6.0 + +### Changed + +- Seek to the begining of the string when using Stream::create() +- Populate ServerRequest::getQueryParams() on instantiation +- Encode [reserved characters](https://www.rfc-editor.org/rfc/rfc3986#appendix-A) in userinfo in Uri +- Normalize leading slashes for Uri::getPath() +- Make Stream's constructor public +- Add some missing type checks on arguments + +## 1.5.1 + +### Fixed + +- Fixed deprecations on PHP 8.1 + +## 1.5.0 + +### Added + +- Add explicit `@return mixed` +- Add explicit return types to HttplugFactory + +### Fixed + +- Improve error handling with streams + +## 1.4.1 + +### Fixed + +- `Psr17Factory::createStreamFromFile`, `UploadedFile::moveTo`, and + `UploadedFile::getStream` no longer throw `ValueError` in PHP 8. + +## 1.4.0 + +### Removed + +The `final` keyword was replaced by `@final` annotation. + +## 1.3.2 + +### Fixed + +- `Stream::read()` must not return boolean. +- Improved exception message when using wrong HTTP status code. + +## 1.3.1 + +### Fixed + +- Allow installation on PHP8 + +## 1.3.0 + +### Added + +- Make Stream::__toString() compatible with throwing exceptions on PHP 7.4. + +### Fixed + +- Support for UTF-8 hostnames +- Support for numeric header names + +## 1.2.1 + +### Changed + +- Added `.github` and `phpstan.neon.dist` to `.gitattributes`. + +## 1.2.0 + +### Changed + +- Change minimal port number to 0 (unix socket) +- Updated `Psr17Factory::createResponse` to respect the specification. If second + argument is not used, a standard reason phrase. If an empty string is passed, + then the reason phrase will be empty. + +### Fixed + +- Check for seekable on the stream resource. +- Fixed the `Response::$reason` should never be null. + +## 1.1.0 + +### Added + +- Improved performance +- More tests for `UploadedFile` and `HttplugFactory` + +### Removed + +- Dead code + +## 1.0.1 + +### Fixed + +- Handle `fopen` failing in createStreamFromFile according to PSR-7. +- Reduce execution path to speed up performance. +- Fixed typos. +- Code style. + +## 1.0.0 + +### Added + +- Support for final PSR-17 (HTTP factories). (`Psr17Factory`) +- Support for numeric header values. +- Support for empty header values. +- All classes are final +- `HttplugFactory` that implements factory interfaces from HTTPlug. + +### Changed + +- `ServerRequest` does not extend `Request`. + +### Removed + +- The HTTPlug discovery strategy was removed since it is included in php-http/discovery 1.4. +- `UploadedFileFactory()` was removed in favor for `Psr17Factory`. +- `ServerRequestFactory()` was removed in favor for `Psr17Factory`. +- `StreamFactory`, `UriFactory`, abd `MessageFactory`. Use `HttplugFactory` instead. +- `ServerRequestFactory::createServerRequestFromArray`, `ServerRequestFactory::createServerRequestFromArrays` and + `ServerRequestFactory::createServerRequestFromGlobals`. Please use the new `nyholm/psr7-server` instead. + +## 0.3.0 + +### Added + +- Return types. +- Many `InvalidArgumentException`s are thrown when you use invalid arguments. +- Integration tests for `UploadedFile` and `ServerRequest`. + +### Changed + +- We dropped PHP7.0 support. +- PSR-17 factories have been marked as internal. They do not fall under our BC promise until PSR-17 is accepted. +- `UploadedFileFactory::createUploadedFile` does not accept a string file path. + +## 0.2.3 + +No changelog before this release diff --git a/vendor/nyholm/psr7/LICENSE b/vendor/nyholm/psr7/LICENSE new file mode 100644 index 000000000..d6c52312c --- /dev/null +++ b/vendor/nyholm/psr7/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 Tobias Nyholm + +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. diff --git a/vendor/nyholm/psr7/README.md b/vendor/nyholm/psr7/README.md new file mode 100644 index 000000000..7fc30bc13 --- /dev/null +++ b/vendor/nyholm/psr7/README.md @@ -0,0 +1,108 @@ +# PSR-7 implementation + +[![Latest Version](https://img.shields.io/github/release/Nyholm/psr7.svg?style=flat-square)](https://github.com/Nyholm/psr7/releases) +[![Total Downloads](https://poser.pugx.org/nyholm/psr7/downloads)](https://packagist.org/packages/nyholm/psr7) +[![Monthly Downloads](https://poser.pugx.org/nyholm/psr7/d/monthly.png)](https://packagist.org/packages/nyholm/psr7) +[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) +[![Static analysis](https://github.com/Nyholm/psr7/actions/workflows/static.yml/badge.svg?branch=master)](https://github.com/Nyholm/psr7/actions/workflows/static.yml?query=branch%3Amaster) +[![Tests](https://github.com/Nyholm/psr7/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/Nyholm/psr7/actions/workflows/tests.yml?query=branch%3Amaster) + +A super lightweight PSR-7 implementation. Very strict and very fast. + +| Description | Guzzle | Laminas | Slim | Nyholm | +| ---- | ------ | ---- | ---- | ------ | +| Lines of code | 3.300 | 3.100 | 1.900 | 1.000 | +| PSR-7* | 66% | 100% | 75% | 100% | +| PSR-17 | No | Yes | Yes | Yes | +| HTTPlug | No | No | No | Yes | +| Performance (runs per second)** | 14.553 | 14.703 | 13.416 | 17.734 | + +\* Percent of completed tests in https://github.com/php-http/psr7-integration-tests + +\** Benchmark with 50.000 runs. See https://github.com/devanych/psr-http-benchmark (higher is better) + +## Installation + +```bash +composer require nyholm/psr7 +``` + +If you are using Symfony Flex then you get all message factories registered as services. + +## Usage + +The PSR-7 objects do not contain any other public methods than those defined in +the [PSR-7 specification](https://www.php-fig.org/psr/psr-7/). + +### Create objects + +Use the PSR-17 factory to create requests, streams, URIs etc. + +```php +$psr17Factory = new \Nyholm\Psr7\Factory\Psr17Factory(); +$request = $psr17Factory->createRequest('GET', 'http://tnyholm.se'); +$stream = $psr17Factory->createStream('foobar'); +``` + +### Sending a request + +With [HTTPlug](http://httplug.io/) or any other PSR-18 (HTTP client) you may send +requests like: + +```bash +composer require kriswallsmith/buzz +``` + +```php +$psr17Factory = new \Nyholm\Psr7\Factory\Psr17Factory(); +$psr18Client = new \Buzz\Client\Curl($psr17Factory); + +$request = $psr17Factory->createRequest('GET', 'http://tnyholm.se'); +$response = $psr18Client->sendRequest($request); +``` + +### Create server requests + +The [`nyholm/psr7-server`](https://github.com/Nyholm/psr7-server) package can be used +to create server requests from PHP superglobals. + +```bash +composer require nyholm/psr7-server +``` + +```php +$psr17Factory = new \Nyholm\Psr7\Factory\Psr17Factory(); + +$creator = new \Nyholm\Psr7Server\ServerRequestCreator( + $psr17Factory, // ServerRequestFactory + $psr17Factory, // UriFactory + $psr17Factory, // UploadedFileFactory + $psr17Factory // StreamFactory +); + +$serverRequest = $creator->fromGlobals(); +``` + +### Emitting a response + +```bash +composer require laminas/laminas-httphandlerrunner +``` + +```php +$psr17Factory = new \Nyholm\Psr7\Factory\Psr17Factory(); + +$responseBody = $psr17Factory->createStream('Hello world'); +$response = $psr17Factory->createResponse(200)->withBody($responseBody); +(new \Laminas\HttpHandlerRunner\Emitter\SapiEmitter())->emit($response); +``` + +## Our goal + +This package is currently maintained by [Tobias Nyholm](http://nyholm.se) and +[Martijn van der Ven](https://vanderven.se/martijn/). They have decided that the +goal of this library should be to provide a super strict implementation of +[PSR-7](https://www.php-fig.org/psr/psr-7/) that is blazing fast. + +The package will never include any extra features nor helper methods. All our classes +and functions exist because they are required to fulfill the PSR-7 specification. diff --git a/vendor/nyholm/psr7/composer.json b/vendor/nyholm/psr7/composer.json new file mode 100644 index 000000000..c6076159d --- /dev/null +++ b/vendor/nyholm/psr7/composer.json @@ -0,0 +1,49 @@ +{ + "name": "nyholm/psr7", + "description": "A fast PHP7 implementation of PSR-7", + "license": "MIT", + "keywords": ["psr-7", "psr-17"], + "homepage": "https://tnyholm.se", + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + }, + { + "name": "Martijn van der Ven", + "email": "martijn@vanderven.se" + } + ], + "require": { + "php": ">=7.2", + "psr/http-message": "^1.1 || ^2.0", + "psr/http-factory": "^1.0" + }, + "require-dev": { + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.4", + "php-http/message-factory": "^1.0", + "php-http/psr7-integration-tests": "^1.0", + "http-interop/http-factory-tests": "^0.9", + "symfony/error-handler": "^4.4" + }, + "provide": { + "php-http/message-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0", + "psr/http-factory-implementation": "1.0" + }, + "autoload": { + "psr-4": { + "Nyholm\\Psr7\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Tests\\Nyholm\\Psr7\\": "tests/" + } + }, + "extra": { + "branch-alias": { + "dev-master": "1.8-dev" + } + } +} diff --git a/vendor/nyholm/psr7/src/Factory/HttplugFactory.php b/vendor/nyholm/psr7/src/Factory/HttplugFactory.php new file mode 100644 index 000000000..cc9285ddf --- /dev/null +++ b/vendor/nyholm/psr7/src/Factory/HttplugFactory.php @@ -0,0 +1,53 @@ + + * @author Martijn van der Ven + * + * @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md + * + * @deprecated since version 1.8, use Psr17Factory instead + */ +class HttplugFactory implements MessageFactory, StreamFactory, UriFactory +{ + public function createRequest($method, $uri, array $headers = [], $body = null, $protocolVersion = '1.1'): RequestInterface + { + return new Request($method, $uri, $headers, $body, $protocolVersion); + } + + public function createResponse($statusCode = 200, $reasonPhrase = null, array $headers = [], $body = null, $version = '1.1'): ResponseInterface + { + return new Response((int) $statusCode, $headers, $body, $version, $reasonPhrase); + } + + public function createStream($body = null): StreamInterface + { + return Stream::create($body ?? ''); + } + + public function createUri($uri = ''): UriInterface + { + if ($uri instanceof UriInterface) { + return $uri; + } + + return new Uri($uri); + } +} diff --git a/vendor/nyholm/psr7/src/Factory/Psr17Factory.php b/vendor/nyholm/psr7/src/Factory/Psr17Factory.php new file mode 100644 index 000000000..2fa98bee3 --- /dev/null +++ b/vendor/nyholm/psr7/src/Factory/Psr17Factory.php @@ -0,0 +1,78 @@ + + * @author Martijn van der Ven + * + * @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md + */ +class Psr17Factory implements RequestFactoryInterface, ResponseFactoryInterface, ServerRequestFactoryInterface, StreamFactoryInterface, UploadedFileFactoryInterface, UriFactoryInterface +{ + public function createRequest(string $method, $uri): RequestInterface + { + return new Request($method, $uri); + } + + public function createResponse(int $code = 200, string $reasonPhrase = ''): ResponseInterface + { + if (2 > \func_num_args()) { + // This will make the Response class to use a custom reasonPhrase + $reasonPhrase = null; + } + + return new Response($code, [], null, '1.1', $reasonPhrase); + } + + public function createStream(string $content = ''): StreamInterface + { + return Stream::create($content); + } + + public function createStreamFromFile(string $filename, string $mode = 'r'): StreamInterface + { + if ('' === $filename) { + throw new \RuntimeException('Path cannot be empty'); + } + + if (false === $resource = @\fopen($filename, $mode)) { + if ('' === $mode || false === \in_array($mode[0], ['r', 'w', 'a', 'x', 'c'], true)) { + throw new \InvalidArgumentException(\sprintf('The mode "%s" is invalid.', $mode)); + } + + throw new \RuntimeException(\sprintf('The file "%s" cannot be opened: %s', $filename, \error_get_last()['message'] ?? '')); + } + + return Stream::create($resource); + } + + public function createStreamFromResource($resource): StreamInterface + { + return Stream::create($resource); + } + + public function createUploadedFile(StreamInterface $stream, ?int $size = null, int $error = \UPLOAD_ERR_OK, ?string $clientFilename = null, ?string $clientMediaType = null): UploadedFileInterface + { + if (null === $size) { + $size = $stream->getSize(); + } + + return new UploadedFile($stream, $size, $error, $clientFilename, $clientMediaType); + } + + public function createUri(string $uri = ''): UriInterface + { + return new Uri($uri); + } + + public function createServerRequest(string $method, $uri, array $serverParams = []): ServerRequestInterface + { + return new ServerRequest($method, $uri, [], null, '1.1', $serverParams); + } +} diff --git a/vendor/nyholm/psr7/src/MessageTrait.php b/vendor/nyholm/psr7/src/MessageTrait.php new file mode 100644 index 000000000..7d02383b8 --- /dev/null +++ b/vendor/nyholm/psr7/src/MessageTrait.php @@ -0,0 +1,235 @@ + + * @author Martijn van der Ven + * + * @internal should not be used outside of Nyholm/Psr7 as it does not fall under our BC promise + */ +trait MessageTrait +{ + /** @var array Map of all registered headers, as original name => array of values */ + private $headers = []; + + /** @var array Map of lowercase header name => original name at registration */ + private $headerNames = []; + + /** @var string */ + private $protocol = '1.1'; + + /** @var StreamInterface|null */ + private $stream; + + public function getProtocolVersion(): string + { + return $this->protocol; + } + + /** + * @return static + */ + public function withProtocolVersion($version): MessageInterface + { + if (!\is_scalar($version)) { + throw new \InvalidArgumentException('Protocol version must be a string'); + } + + if ($this->protocol === $version) { + return $this; + } + + $new = clone $this; + $new->protocol = (string) $version; + + return $new; + } + + public function getHeaders(): array + { + return $this->headers; + } + + public function hasHeader($header): bool + { + return isset($this->headerNames[\strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')]); + } + + public function getHeader($header): array + { + if (!\is_string($header)) { + throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string'); + } + + $header = \strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); + if (!isset($this->headerNames[$header])) { + return []; + } + + $header = $this->headerNames[$header]; + + return $this->headers[$header]; + } + + public function getHeaderLine($header): string + { + return \implode(', ', $this->getHeader($header)); + } + + /** + * @return static + */ + public function withHeader($header, $value): MessageInterface + { + $value = $this->validateAndTrimHeader($header, $value); + $normalized = \strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); + + $new = clone $this; + if (isset($new->headerNames[$normalized])) { + unset($new->headers[$new->headerNames[$normalized]]); + } + $new->headerNames[$normalized] = $header; + $new->headers[$header] = $value; + + return $new; + } + + /** + * @return static + */ + public function withAddedHeader($header, $value): MessageInterface + { + if (!\is_string($header) || '' === $header) { + throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string'); + } + + $new = clone $this; + $new->setHeaders([$header => $value]); + + return $new; + } + + /** + * @return static + */ + public function withoutHeader($header): MessageInterface + { + if (!\is_string($header)) { + throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string'); + } + + $normalized = \strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); + if (!isset($this->headerNames[$normalized])) { + return $this; + } + + $header = $this->headerNames[$normalized]; + $new = clone $this; + unset($new->headers[$header], $new->headerNames[$normalized]); + + return $new; + } + + public function getBody(): StreamInterface + { + if (null === $this->stream) { + $this->stream = Stream::create(''); + } + + return $this->stream; + } + + /** + * @return static + */ + public function withBody(StreamInterface $body): MessageInterface + { + if ($body === $this->stream) { + return $this; + } + + $new = clone $this; + $new->stream = $body; + + return $new; + } + + private function setHeaders(array $headers): void + { + foreach ($headers as $header => $value) { + if (\is_int($header)) { + // If a header name was set to a numeric string, PHP will cast the key to an int. + // We must cast it back to a string in order to comply with validation. + $header = (string) $header; + } + $value = $this->validateAndTrimHeader($header, $value); + $normalized = \strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); + if (isset($this->headerNames[$normalized])) { + $header = $this->headerNames[$normalized]; + $this->headers[$header] = \array_merge($this->headers[$header], $value); + } else { + $this->headerNames[$normalized] = $header; + $this->headers[$header] = $value; + } + } + } + + /** + * Make sure the header complies with RFC 7230. + * + * Header names must be a non-empty string consisting of token characters. + * + * Header values must be strings consisting of visible characters with all optional + * leading and trailing whitespace stripped. This method will always strip such + * optional whitespace. Note that the method does not allow folding whitespace within + * the values as this was deprecated for almost all instances by the RFC. + * + * header-field = field-name ":" OWS field-value OWS + * field-name = 1*( "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / "^" + * / "_" / "`" / "|" / "~" / %x30-39 / ( %x41-5A / %x61-7A ) ) + * OWS = *( SP / HTAB ) + * field-value = *( ( %x21-7E / %x80-FF ) [ 1*( SP / HTAB ) ( %x21-7E / %x80-FF ) ] ) + * + * @see https://tools.ietf.org/html/rfc7230#section-3.2.4 + */ + private function validateAndTrimHeader($header, $values): array + { + if (!\is_string($header) || 1 !== \preg_match("@^[!#$%&'*+.^_`|~0-9A-Za-z-]+$@D", $header)) { + throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string'); + } + + if (!\is_array($values)) { + // This is simple, just one value. + if ((!\is_numeric($values) && !\is_string($values)) || 1 !== \preg_match("@^[ \t\x21-\x7E\x80-\xFF]*$@", (string) $values)) { + throw new \InvalidArgumentException('Header values must be RFC 7230 compatible strings'); + } + + return [\trim((string) $values, " \t")]; + } + + if (empty($values)) { + throw new \InvalidArgumentException('Header values must be a string or an array of strings, empty array given'); + } + + // Assert Non empty array + $returnValues = []; + foreach ($values as $v) { + if ((!\is_numeric($v) && !\is_string($v)) || 1 !== \preg_match("@^[ \t\x21-\x7E\x80-\xFF]*$@D", (string) $v)) { + throw new \InvalidArgumentException('Header values must be RFC 7230 compatible strings'); + } + + $returnValues[] = \trim((string) $v, " \t"); + } + + return $returnValues; + } +} diff --git a/vendor/nyholm/psr7/src/Request.php b/vendor/nyholm/psr7/src/Request.php new file mode 100644 index 000000000..d50744eec --- /dev/null +++ b/vendor/nyholm/psr7/src/Request.php @@ -0,0 +1,47 @@ + + * @author Martijn van der Ven + * + * @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md + */ +class Request implements RequestInterface +{ + use MessageTrait; + use RequestTrait; + + /** + * @param string $method HTTP method + * @param string|UriInterface $uri URI + * @param array $headers Request headers + * @param string|resource|StreamInterface|null $body Request body + * @param string $version Protocol version + */ + public function __construct(string $method, $uri, array $headers = [], $body = null, string $version = '1.1') + { + if (!($uri instanceof UriInterface)) { + $uri = new Uri($uri); + } + + $this->method = $method; + $this->uri = $uri; + $this->setHeaders($headers); + $this->protocol = $version; + + if (!$this->hasHeader('Host')) { + $this->updateHostFromUri(); + } + + // If we got no body, defer initialization of the stream until Request::getBody() + if ('' !== $body && null !== $body) { + $this->stream = Stream::create($body); + } + } +} diff --git a/vendor/nyholm/psr7/src/RequestTrait.php b/vendor/nyholm/psr7/src/RequestTrait.php new file mode 100644 index 000000000..2dbb3abf6 --- /dev/null +++ b/vendor/nyholm/psr7/src/RequestTrait.php @@ -0,0 +1,127 @@ + + * @author Martijn van der Ven + * + * @internal should not be used outside of Nyholm/Psr7 as it does not fall under our BC promise + */ +trait RequestTrait +{ + /** @var string */ + private $method; + + /** @var string|null */ + private $requestTarget; + + /** @var UriInterface|null */ + private $uri; + + public function getRequestTarget(): string + { + if (null !== $this->requestTarget) { + return $this->requestTarget; + } + + if ('' === $target = $this->uri->getPath()) { + $target = '/'; + } + if ('' !== $this->uri->getQuery()) { + $target .= '?' . $this->uri->getQuery(); + } + + return $target; + } + + /** + * @return static + */ + public function withRequestTarget($requestTarget): RequestInterface + { + if (!\is_string($requestTarget)) { + throw new \InvalidArgumentException('Request target must be a string'); + } + + if (\preg_match('#\s#', $requestTarget)) { + throw new \InvalidArgumentException('Invalid request target provided; cannot contain whitespace'); + } + + $new = clone $this; + $new->requestTarget = $requestTarget; + + return $new; + } + + public function getMethod(): string + { + return $this->method; + } + + /** + * @return static + */ + public function withMethod($method): RequestInterface + { + if (!\is_string($method)) { + throw new \InvalidArgumentException('Method must be a string'); + } + + $new = clone $this; + $new->method = $method; + + return $new; + } + + public function getUri(): UriInterface + { + return $this->uri; + } + + /** + * @return static + */ + public function withUri(UriInterface $uri, $preserveHost = false): RequestInterface + { + if ($uri === $this->uri) { + return $this; + } + + $new = clone $this; + $new->uri = $uri; + + if (!$preserveHost || !$this->hasHeader('Host')) { + $new->updateHostFromUri(); + } + + return $new; + } + + private function updateHostFromUri(): void + { + if ('' === $host = $this->uri->getHost()) { + return; + } + + if (null !== ($port = $this->uri->getPort())) { + $host .= ':' . $port; + } + + if (isset($this->headerNames['host'])) { + $header = $this->headerNames['host']; + } else { + $this->headerNames['host'] = $header = 'Host'; + } + + // Ensure Host is the first header. + // See: http://tools.ietf.org/html/rfc7230#section-5.4 + $this->headers = [$header => [$host]] + $this->headers; + } +} diff --git a/vendor/nyholm/psr7/src/Response.php b/vendor/nyholm/psr7/src/Response.php new file mode 100644 index 000000000..71eb2fa49 --- /dev/null +++ b/vendor/nyholm/psr7/src/Response.php @@ -0,0 +1,93 @@ + + * @author Martijn van der Ven + * + * @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md + */ +class Response implements ResponseInterface +{ + use MessageTrait; + + /** @var array Map of standard HTTP status code/reason phrases */ + private const PHRASES = [ + 100 => 'Continue', 101 => 'Switching Protocols', 102 => 'Processing', + 200 => 'OK', 201 => 'Created', 202 => 'Accepted', 203 => 'Non-Authoritative Information', 204 => 'No Content', 205 => 'Reset Content', 206 => 'Partial Content', 207 => 'Multi-status', 208 => 'Already Reported', + 300 => 'Multiple Choices', 301 => 'Moved Permanently', 302 => 'Found', 303 => 'See Other', 304 => 'Not Modified', 305 => 'Use Proxy', 306 => 'Switch Proxy', 307 => 'Temporary Redirect', + 400 => 'Bad Request', 401 => 'Unauthorized', 402 => 'Payment Required', 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', 406 => 'Not Acceptable', 407 => 'Proxy Authentication Required', 408 => 'Request Time-out', 409 => 'Conflict', 410 => 'Gone', 411 => 'Length Required', 412 => 'Precondition Failed', 413 => 'Request Entity Too Large', 414 => 'Request-URI Too Large', 415 => 'Unsupported Media Type', 416 => 'Requested range not satisfiable', 417 => 'Expectation Failed', 418 => 'I\'m a teapot', 422 => 'Unprocessable Entity', 423 => 'Locked', 424 => 'Failed Dependency', 425 => 'Unordered Collection', 426 => 'Upgrade Required', 428 => 'Precondition Required', 429 => 'Too Many Requests', 431 => 'Request Header Fields Too Large', 451 => 'Unavailable For Legal Reasons', + 500 => 'Internal Server Error', 501 => 'Not Implemented', 502 => 'Bad Gateway', 503 => 'Service Unavailable', 504 => 'Gateway Time-out', 505 => 'HTTP Version not supported', 506 => 'Variant Also Negotiates', 507 => 'Insufficient Storage', 508 => 'Loop Detected', 511 => 'Network Authentication Required', + ]; + + /** @var string */ + private $reasonPhrase = ''; + + /** @var int */ + private $statusCode; + + /** + * @param int $status Status code + * @param array $headers Response headers + * @param string|resource|StreamInterface|null $body Response body + * @param string $version Protocol version + * @param string|null $reason Reason phrase (when empty a default will be used based on the status code) + */ + public function __construct(int $status = 200, array $headers = [], $body = null, string $version = '1.1', ?string $reason = null) + { + // If we got no body, defer initialization of the stream until Response::getBody() + if ('' !== $body && null !== $body) { + $this->stream = Stream::create($body); + } + + $this->statusCode = $status; + $this->setHeaders($headers); + if (null === $reason && isset(self::PHRASES[$this->statusCode])) { + $this->reasonPhrase = self::PHRASES[$status]; + } else { + $this->reasonPhrase = $reason ?? ''; + } + + $this->protocol = $version; + } + + public function getStatusCode(): int + { + return $this->statusCode; + } + + public function getReasonPhrase(): string + { + return $this->reasonPhrase; + } + + /** + * @return static + */ + public function withStatus($code, $reasonPhrase = ''): ResponseInterface + { + if (!\is_int($code) && !\is_string($code)) { + throw new \InvalidArgumentException('Status code has to be an integer'); + } + + $code = (int) $code; + if ($code < 100 || $code > 599) { + throw new \InvalidArgumentException(\sprintf('Status code has to be an integer between 100 and 599. A status code of %d was given', $code)); + } + + $new = clone $this; + $new->statusCode = $code; + if ((null === $reasonPhrase || '' === $reasonPhrase) && isset(self::PHRASES[$new->statusCode])) { + $reasonPhrase = self::PHRASES[$new->statusCode]; + } + $new->reasonPhrase = $reasonPhrase; + + return $new; + } +} diff --git a/vendor/nyholm/psr7/src/ServerRequest.php b/vendor/nyholm/psr7/src/ServerRequest.php new file mode 100644 index 000000000..a3c5ba90b --- /dev/null +++ b/vendor/nyholm/psr7/src/ServerRequest.php @@ -0,0 +1,201 @@ + + * @author Martijn van der Ven + * + * @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md + */ +class ServerRequest implements ServerRequestInterface +{ + use MessageTrait; + use RequestTrait; + + /** @var array */ + private $attributes = []; + + /** @var array */ + private $cookieParams = []; + + /** @var array|object|null */ + private $parsedBody; + + /** @var array */ + private $queryParams = []; + + /** @var array */ + private $serverParams; + + /** @var UploadedFileInterface[] */ + private $uploadedFiles = []; + + /** + * @param string $method HTTP method + * @param string|UriInterface $uri URI + * @param array $headers Request headers + * @param string|resource|StreamInterface|null $body Request body + * @param string $version Protocol version + * @param array $serverParams Typically the $_SERVER superglobal + */ + public function __construct(string $method, $uri, array $headers = [], $body = null, string $version = '1.1', array $serverParams = []) + { + $this->serverParams = $serverParams; + + if (!($uri instanceof UriInterface)) { + $uri = new Uri($uri); + } + + $this->method = $method; + $this->uri = $uri; + $this->setHeaders($headers); + $this->protocol = $version; + \parse_str($uri->getQuery(), $this->queryParams); + + if (!$this->hasHeader('Host')) { + $this->updateHostFromUri(); + } + + // If we got no body, defer initialization of the stream until ServerRequest::getBody() + if ('' !== $body && null !== $body) { + $this->stream = Stream::create($body); + } + } + + public function getServerParams(): array + { + return $this->serverParams; + } + + public function getUploadedFiles(): array + { + return $this->uploadedFiles; + } + + /** + * @return static + */ + public function withUploadedFiles(array $uploadedFiles): ServerRequestInterface + { + $new = clone $this; + $new->uploadedFiles = $uploadedFiles; + + return $new; + } + + public function getCookieParams(): array + { + return $this->cookieParams; + } + + /** + * @return static + */ + public function withCookieParams(array $cookies): ServerRequestInterface + { + $new = clone $this; + $new->cookieParams = $cookies; + + return $new; + } + + public function getQueryParams(): array + { + return $this->queryParams; + } + + /** + * @return static + */ + public function withQueryParams(array $query): ServerRequestInterface + { + $new = clone $this; + $new->queryParams = $query; + + return $new; + } + + /** + * @return array|object|null + */ + public function getParsedBody() + { + return $this->parsedBody; + } + + /** + * @return static + */ + public function withParsedBody($data): ServerRequestInterface + { + if (!\is_array($data) && !\is_object($data) && null !== $data) { + throw new \InvalidArgumentException('First parameter to withParsedBody MUST be object, array or null'); + } + + $new = clone $this; + $new->parsedBody = $data; + + return $new; + } + + public function getAttributes(): array + { + return $this->attributes; + } + + /** + * @return mixed + */ + public function getAttribute($attribute, $default = null) + { + if (!\is_string($attribute)) { + throw new \InvalidArgumentException('Attribute name must be a string'); + } + + if (false === \array_key_exists($attribute, $this->attributes)) { + return $default; + } + + return $this->attributes[$attribute]; + } + + /** + * @return static + */ + public function withAttribute($attribute, $value): ServerRequestInterface + { + if (!\is_string($attribute)) { + throw new \InvalidArgumentException('Attribute name must be a string'); + } + + $new = clone $this; + $new->attributes[$attribute] = $value; + + return $new; + } + + /** + * @return static + */ + public function withoutAttribute($attribute): ServerRequestInterface + { + if (!\is_string($attribute)) { + throw new \InvalidArgumentException('Attribute name must be a string'); + } + + if (false === \array_key_exists($attribute, $this->attributes)) { + return $this; + } + + $new = clone $this; + unset($new->attributes[$attribute]); + + return $new; + } +} diff --git a/vendor/nyholm/psr7/src/Stream.php b/vendor/nyholm/psr7/src/Stream.php new file mode 100644 index 000000000..63b7d6dd5 --- /dev/null +++ b/vendor/nyholm/psr7/src/Stream.php @@ -0,0 +1,399 @@ + + * @author Martijn van der Ven + * + * @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md + */ +class Stream implements StreamInterface +{ + use StreamTrait; + + /** @var resource|null A resource reference */ + private $stream; + + /** @var bool */ + private $seekable; + + /** @var bool */ + private $readable; + + /** @var bool */ + private $writable; + + /** @var array|mixed|void|bool|null */ + private $uri; + + /** @var int|null */ + private $size; + + /** @var array Hash of readable and writable stream types */ + private const READ_WRITE_HASH = [ + 'read' => [ + 'r' => true, 'w+' => true, 'r+' => true, 'x+' => true, 'c+' => true, + 'rb' => true, 'w+b' => true, 'r+b' => true, 'x+b' => true, + 'c+b' => true, 'rt' => true, 'w+t' => true, 'r+t' => true, + 'x+t' => true, 'c+t' => true, 'a+' => true, + ], + 'write' => [ + 'w' => true, 'w+' => true, 'rw' => true, 'r+' => true, 'x+' => true, + 'c+' => true, 'wb' => true, 'w+b' => true, 'r+b' => true, + 'x+b' => true, 'c+b' => true, 'w+t' => true, 'r+t' => true, + 'x+t' => true, 'c+t' => true, 'a' => true, 'a+' => true, + ], + ]; + + /** + * @param resource $body + */ + public function __construct($body) + { + if (!\is_resource($body)) { + throw new \InvalidArgumentException('First argument to Stream::__construct() must be resource'); + } + + $this->stream = $body; + $meta = \stream_get_meta_data($this->stream); + $this->seekable = $meta['seekable'] && 0 === \fseek($this->stream, 0, \SEEK_CUR); + $this->readable = isset(self::READ_WRITE_HASH['read'][$meta['mode']]); + $this->writable = isset(self::READ_WRITE_HASH['write'][$meta['mode']]); + } + + /** + * Creates a new PSR-7 stream. + * + * @param string|resource|StreamInterface $body + * + * @throws \InvalidArgumentException + */ + public static function create($body = ''): StreamInterface + { + if ($body instanceof StreamInterface) { + return $body; + } + + if (\is_string($body)) { + if (200000 <= \strlen($body)) { + $body = self::openZvalStream($body); + } else { + $resource = \fopen('php://memory', 'r+'); + \fwrite($resource, $body); + \fseek($resource, 0); + $body = $resource; + } + } + + if (!\is_resource($body)) { + throw new \InvalidArgumentException('First argument to Stream::create() must be a string, resource or StreamInterface'); + } + + return new self($body); + } + + /** + * Closes the stream when the destructed. + */ + public function __destruct() + { + $this->close(); + } + + public function close(): void + { + if (isset($this->stream)) { + if (\is_resource($this->stream)) { + \fclose($this->stream); + } + $this->detach(); + } + } + + public function detach() + { + if (!isset($this->stream)) { + return null; + } + + $result = $this->stream; + unset($this->stream); + $this->size = $this->uri = null; + $this->readable = $this->writable = $this->seekable = false; + + return $result; + } + + private function getUri() + { + if (false !== $this->uri) { + $this->uri = $this->getMetadata('uri') ?? false; + } + + return $this->uri; + } + + public function getSize(): ?int + { + if (null !== $this->size) { + return $this->size; + } + + if (!isset($this->stream)) { + return null; + } + + // Clear the stat cache if the stream has a URI + if ($uri = $this->getUri()) { + \clearstatcache(true, $uri); + } + + $stats = \fstat($this->stream); + if (isset($stats['size'])) { + $this->size = $stats['size']; + + return $this->size; + } + + return null; + } + + public function tell(): int + { + if (!isset($this->stream)) { + throw new \RuntimeException('Stream is detached'); + } + + if (false === $result = @\ftell($this->stream)) { + throw new \RuntimeException('Unable to determine stream position: ' . (\error_get_last()['message'] ?? '')); + } + + return $result; + } + + public function eof(): bool + { + return !isset($this->stream) || \feof($this->stream); + } + + public function isSeekable(): bool + { + return $this->seekable; + } + + public function seek($offset, $whence = \SEEK_SET): void + { + if (!isset($this->stream)) { + throw new \RuntimeException('Stream is detached'); + } + + if (!$this->seekable) { + throw new \RuntimeException('Stream is not seekable'); + } + + if (-1 === \fseek($this->stream, $offset, $whence)) { + throw new \RuntimeException('Unable to seek to stream position "' . $offset . '" with whence ' . \var_export($whence, true)); + } + } + + public function rewind(): void + { + $this->seek(0); + } + + public function isWritable(): bool + { + return $this->writable; + } + + public function write($string): int + { + if (!isset($this->stream)) { + throw new \RuntimeException('Stream is detached'); + } + + if (!$this->writable) { + throw new \RuntimeException('Cannot write to a non-writable stream'); + } + + // We can't know the size after writing anything + $this->size = null; + + if (false === $result = @\fwrite($this->stream, $string)) { + throw new \RuntimeException('Unable to write to stream: ' . (\error_get_last()['message'] ?? '')); + } + + return $result; + } + + public function isReadable(): bool + { + return $this->readable; + } + + public function read($length): string + { + if (!isset($this->stream)) { + throw new \RuntimeException('Stream is detached'); + } + + if (!$this->readable) { + throw new \RuntimeException('Cannot read from non-readable stream'); + } + + if (false === $result = @\fread($this->stream, $length)) { + throw new \RuntimeException('Unable to read from stream: ' . (\error_get_last()['message'] ?? '')); + } + + return $result; + } + + public function getContents(): string + { + if (!isset($this->stream)) { + throw new \RuntimeException('Stream is detached'); + } + + $exception = null; + + \set_error_handler(static function ($type, $message) use (&$exception) { + throw $exception = new \RuntimeException('Unable to read stream contents: ' . $message); + }); + + try { + return \stream_get_contents($this->stream); + } catch (\Throwable $e) { + throw $e === $exception ? $e : new \RuntimeException('Unable to read stream contents: ' . $e->getMessage(), 0, $e); + } finally { + \restore_error_handler(); + } + } + + /** + * @return mixed + */ + public function getMetadata($key = null) + { + if (null !== $key && !\is_string($key)) { + throw new \InvalidArgumentException('Metadata key must be a string'); + } + + if (!isset($this->stream)) { + return $key ? null : []; + } + + $meta = \stream_get_meta_data($this->stream); + + if (null === $key) { + return $meta; + } + + return $meta[$key] ?? null; + } + + private static function openZvalStream(string $body) + { + static $wrapper; + + $wrapper ?? \stream_wrapper_register('Nyholm-Psr7-Zval', $wrapper = \get_class(new class() { + public $context; + + private $data; + private $position = 0; + + public function stream_open(): bool + { + $this->data = \stream_context_get_options($this->context)['Nyholm-Psr7-Zval']['data']; + \stream_context_set_option($this->context, 'Nyholm-Psr7-Zval', 'data', null); + + return true; + } + + public function stream_read(int $count): string + { + $result = \substr($this->data, $this->position, $count); + $this->position += \strlen($result); + + return $result; + } + + public function stream_write(string $data): int + { + $this->data = \substr_replace($this->data, $data, $this->position, \strlen($data)); + $this->position += \strlen($data); + + return \strlen($data); + } + + public function stream_tell(): int + { + return $this->position; + } + + public function stream_eof(): bool + { + return \strlen($this->data) <= $this->position; + } + + public function stream_stat(): array + { + return [ + 'mode' => 33206, // POSIX_S_IFREG | 0666 + 'nlink' => 1, + 'rdev' => -1, + 'size' => \strlen($this->data), + 'blksize' => -1, + 'blocks' => -1, + ]; + } + + public function stream_seek(int $offset, int $whence): bool + { + if (\SEEK_SET === $whence && (0 <= $offset && \strlen($this->data) >= $offset)) { + $this->position = $offset; + } elseif (\SEEK_CUR === $whence && 0 <= $offset) { + $this->position += $offset; + } elseif (\SEEK_END === $whence && (0 > $offset && 0 <= $offset = \strlen($this->data) + $offset)) { + $this->position = $offset; + } else { + return false; + } + + return true; + } + + public function stream_set_option(): bool + { + return true; + } + + public function stream_truncate(int $new_size): bool + { + if ($new_size) { + $this->data = \substr($this->data, 0, $new_size); + $this->position = \min($this->position, $new_size); + } else { + $this->data = ''; + $this->position = 0; + } + + return true; + } + })); + + $context = \stream_context_create(['Nyholm-Psr7-Zval' => ['data' => $body]]); + + if (!$stream = @\fopen('Nyholm-Psr7-Zval://', 'r+', false, $context)) { + \stream_wrapper_register('Nyholm-Psr7-Zval', $wrapper); + $stream = \fopen('Nyholm-Psr7-Zval://', 'r+', false, $context); + } + + return $stream; + } +} diff --git a/vendor/nyholm/psr7/src/StreamTrait.php b/vendor/nyholm/psr7/src/StreamTrait.php new file mode 100644 index 000000000..41a3f9d7e --- /dev/null +++ b/vendor/nyholm/psr7/src/StreamTrait.php @@ -0,0 +1,57 @@ += 70400 || (new \ReflectionMethod(StreamInterface::class, '__toString'))->hasReturnType()) { + /** + * @internal + */ + trait StreamTrait + { + public function __toString(): string + { + if ($this->isSeekable()) { + $this->seek(0); + } + + return $this->getContents(); + } + } +} else { + /** + * @internal + */ + trait StreamTrait + { + /** + * @return string + */ + public function __toString() + { + try { + if ($this->isSeekable()) { + $this->seek(0); + } + + return $this->getContents(); + } catch (\Throwable $e) { + if (\is_array($errorHandler = \set_error_handler('var_dump'))) { + $errorHandler = $errorHandler[0] ?? null; + } + \restore_error_handler(); + + if ($e instanceof \Error || $errorHandler instanceof SymfonyErrorHandler || $errorHandler instanceof SymfonyLegacyErrorHandler) { + return \trigger_error((string) $e, \E_USER_ERROR); + } + + return ''; + } + } + } +} diff --git a/vendor/nyholm/psr7/src/UploadedFile.php b/vendor/nyholm/psr7/src/UploadedFile.php new file mode 100644 index 000000000..c77dca43f --- /dev/null +++ b/vendor/nyholm/psr7/src/UploadedFile.php @@ -0,0 +1,179 @@ + + * @author Martijn van der Ven + * + * @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md + */ +class UploadedFile implements UploadedFileInterface +{ + /** @var array */ + private const ERRORS = [ + \UPLOAD_ERR_OK => 1, + \UPLOAD_ERR_INI_SIZE => 1, + \UPLOAD_ERR_FORM_SIZE => 1, + \UPLOAD_ERR_PARTIAL => 1, + \UPLOAD_ERR_NO_FILE => 1, + \UPLOAD_ERR_NO_TMP_DIR => 1, + \UPLOAD_ERR_CANT_WRITE => 1, + \UPLOAD_ERR_EXTENSION => 1, + ]; + + /** @var string */ + private $clientFilename; + + /** @var string */ + private $clientMediaType; + + /** @var int */ + private $error; + + /** @var string|null */ + private $file; + + /** @var bool */ + private $moved = false; + + /** @var int */ + private $size; + + /** @var StreamInterface|null */ + private $stream; + + /** + * @param StreamInterface|string|resource $streamOrFile + * @param int $size + * @param int $errorStatus + * @param string|null $clientFilename + * @param string|null $clientMediaType + */ + public function __construct($streamOrFile, $size, $errorStatus, $clientFilename = null, $clientMediaType = null) + { + if (false === \is_int($errorStatus) || !isset(self::ERRORS[$errorStatus])) { + throw new \InvalidArgumentException('Upload file error status must be an integer value and one of the "UPLOAD_ERR_*" constants'); + } + + if (false === \is_int($size)) { + throw new \InvalidArgumentException('Upload file size must be an integer'); + } + + if (null !== $clientFilename && !\is_string($clientFilename)) { + throw new \InvalidArgumentException('Upload file client filename must be a string or null'); + } + + if (null !== $clientMediaType && !\is_string($clientMediaType)) { + throw new \InvalidArgumentException('Upload file client media type must be a string or null'); + } + + $this->error = $errorStatus; + $this->size = $size; + $this->clientFilename = $clientFilename; + $this->clientMediaType = $clientMediaType; + + if (\UPLOAD_ERR_OK === $this->error) { + // Depending on the value set file or stream variable. + if (\is_string($streamOrFile) && '' !== $streamOrFile) { + $this->file = $streamOrFile; + } elseif (\is_resource($streamOrFile)) { + $this->stream = Stream::create($streamOrFile); + } elseif ($streamOrFile instanceof StreamInterface) { + $this->stream = $streamOrFile; + } else { + throw new \InvalidArgumentException('Invalid stream or file provided for UploadedFile'); + } + } + } + + /** + * @throws \RuntimeException if is moved or not ok + */ + private function validateActive(): void + { + if (\UPLOAD_ERR_OK !== $this->error) { + throw new \RuntimeException('Cannot retrieve stream due to upload error'); + } + + if ($this->moved) { + throw new \RuntimeException('Cannot retrieve stream after it has already been moved'); + } + } + + public function getStream(): StreamInterface + { + $this->validateActive(); + + if ($this->stream instanceof StreamInterface) { + return $this->stream; + } + + if (false === $resource = @\fopen($this->file, 'r')) { + throw new \RuntimeException(\sprintf('The file "%s" cannot be opened: %s', $this->file, \error_get_last()['message'] ?? '')); + } + + return Stream::create($resource); + } + + public function moveTo($targetPath): void + { + $this->validateActive(); + + if (!\is_string($targetPath) || '' === $targetPath) { + throw new \InvalidArgumentException('Invalid path provided for move operation; must be a non-empty string'); + } + + if (null !== $this->file) { + $this->moved = 'cli' === \PHP_SAPI ? @\rename($this->file, $targetPath) : @\move_uploaded_file($this->file, $targetPath); + + if (false === $this->moved) { + throw new \RuntimeException(\sprintf('Uploaded file could not be moved to "%s": %s', $targetPath, \error_get_last()['message'] ?? '')); + } + } else { + $stream = $this->getStream(); + if ($stream->isSeekable()) { + $stream->rewind(); + } + + if (false === $resource = @\fopen($targetPath, 'w')) { + throw new \RuntimeException(\sprintf('The file "%s" cannot be opened: %s', $targetPath, \error_get_last()['message'] ?? '')); + } + + $dest = Stream::create($resource); + + while (!$stream->eof()) { + if (!$dest->write($stream->read(1048576))) { + break; + } + } + + $this->moved = true; + } + } + + public function getSize(): int + { + return $this->size; + } + + public function getError(): int + { + return $this->error; + } + + public function getClientFilename(): ?string + { + return $this->clientFilename; + } + + public function getClientMediaType(): ?string + { + return $this->clientMediaType; + } +} diff --git a/vendor/nyholm/psr7/src/Uri.php b/vendor/nyholm/psr7/src/Uri.php new file mode 100644 index 000000000..621e2e724 --- /dev/null +++ b/vendor/nyholm/psr7/src/Uri.php @@ -0,0 +1,356 @@ + + * @author Martijn van der Ven + * + * @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md + */ +class Uri implements UriInterface +{ + private const SCHEMES = ['http' => 80, 'https' => 443]; + + private const CHAR_UNRESERVED = 'a-zA-Z0-9_\-\.~'; + + private const CHAR_SUB_DELIMS = '!\$&\'\(\)\*\+,;='; + + private const CHAR_GEN_DELIMS = ':\/\?#\[\]@'; + + /** @var string Uri scheme. */ + private $scheme = ''; + + /** @var string Uri user info. */ + private $userInfo = ''; + + /** @var string Uri host. */ + private $host = ''; + + /** @var int|null Uri port. */ + private $port; + + /** @var string Uri path. */ + private $path = ''; + + /** @var string Uri query string. */ + private $query = ''; + + /** @var string Uri fragment. */ + private $fragment = ''; + + public function __construct(string $uri = '') + { + if ('' !== $uri) { + if (false === $parts = \parse_url($uri)) { + throw new \InvalidArgumentException(\sprintf('Unable to parse URI: "%s"', $uri)); + } + + // Apply parse_url parts to a URI. + $this->scheme = isset($parts['scheme']) ? \strtr($parts['scheme'], 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz') : ''; + $this->userInfo = $parts['user'] ?? ''; + $this->host = isset($parts['host']) ? \strtr($parts['host'], 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz') : ''; + $this->port = isset($parts['port']) ? $this->filterPort($parts['port']) : null; + $this->path = isset($parts['path']) ? $this->filterPath($parts['path']) : ''; + $this->query = isset($parts['query']) ? $this->filterQueryAndFragment($parts['query']) : ''; + $this->fragment = isset($parts['fragment']) ? $this->filterQueryAndFragment($parts['fragment']) : ''; + if (isset($parts['pass'])) { + $this->userInfo .= ':' . $parts['pass']; + } + } + } + + public function __toString(): string + { + return self::createUriString($this->scheme, $this->getAuthority(), $this->path, $this->query, $this->fragment); + } + + public function getScheme(): string + { + return $this->scheme; + } + + public function getAuthority(): string + { + if ('' === $this->host) { + return ''; + } + + $authority = $this->host; + if ('' !== $this->userInfo) { + $authority = $this->userInfo . '@' . $authority; + } + + if (null !== $this->port) { + $authority .= ':' . $this->port; + } + + return $authority; + } + + public function getUserInfo(): string + { + return $this->userInfo; + } + + public function getHost(): string + { + return $this->host; + } + + public function getPort(): ?int + { + return $this->port; + } + + public function getPath(): string + { + $path = $this->path; + + if ('' !== $path && '/' !== $path[0]) { + if ('' !== $this->host) { + // If the path is rootless and an authority is present, the path MUST be prefixed by "/" + $path = '/' . $path; + } + } elseif (isset($path[1]) && '/' === $path[1]) { + // If the path is starting with more than one "/", the + // starting slashes MUST be reduced to one. + $path = '/' . \ltrim($path, '/'); + } + + return $path; + } + + public function getQuery(): string + { + return $this->query; + } + + public function getFragment(): string + { + return $this->fragment; + } + + /** + * @return static + */ + public function withScheme($scheme): UriInterface + { + if (!\is_string($scheme)) { + throw new \InvalidArgumentException('Scheme must be a string'); + } + + if ($this->scheme === $scheme = \strtr($scheme, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')) { + return $this; + } + + $new = clone $this; + $new->scheme = $scheme; + $new->port = $new->filterPort($new->port); + + return $new; + } + + /** + * @return static + */ + public function withUserInfo($user, $password = null): UriInterface + { + if (!\is_string($user)) { + throw new \InvalidArgumentException('User must be a string'); + } + + $info = \preg_replace_callback('/[' . self::CHAR_GEN_DELIMS . self::CHAR_SUB_DELIMS . ']++/', [__CLASS__, 'rawurlencodeMatchZero'], $user); + if (null !== $password && '' !== $password) { + if (!\is_string($password)) { + throw new \InvalidArgumentException('Password must be a string'); + } + + $info .= ':' . \preg_replace_callback('/[' . self::CHAR_GEN_DELIMS . self::CHAR_SUB_DELIMS . ']++/', [__CLASS__, 'rawurlencodeMatchZero'], $password); + } + + if ($this->userInfo === $info) { + return $this; + } + + $new = clone $this; + $new->userInfo = $info; + + return $new; + } + + /** + * @return static + */ + public function withHost($host): UriInterface + { + if (!\is_string($host)) { + throw new \InvalidArgumentException('Host must be a string'); + } + + if ($this->host === $host = \strtr($host, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')) { + return $this; + } + + $new = clone $this; + $new->host = $host; + + return $new; + } + + /** + * @return static + */ + public function withPort($port): UriInterface + { + if ($this->port === $port = $this->filterPort($port)) { + return $this; + } + + $new = clone $this; + $new->port = $port; + + return $new; + } + + /** + * @return static + */ + public function withPath($path): UriInterface + { + if ($this->path === $path = $this->filterPath($path)) { + return $this; + } + + $new = clone $this; + $new->path = $path; + + return $new; + } + + /** + * @return static + */ + public function withQuery($query): UriInterface + { + if ($this->query === $query = $this->filterQueryAndFragment($query)) { + return $this; + } + + $new = clone $this; + $new->query = $query; + + return $new; + } + + /** + * @return static + */ + public function withFragment($fragment): UriInterface + { + if ($this->fragment === $fragment = $this->filterQueryAndFragment($fragment)) { + return $this; + } + + $new = clone $this; + $new->fragment = $fragment; + + return $new; + } + + /** + * Create a URI string from its various parts. + */ + private static function createUriString(string $scheme, string $authority, string $path, string $query, string $fragment): string + { + $uri = ''; + if ('' !== $scheme) { + $uri .= $scheme . ':'; + } + + if ('' !== $authority) { + $uri .= '//' . $authority; + } + + if ('' !== $path) { + if ('/' !== $path[0]) { + if ('' !== $authority) { + // If the path is rootless and an authority is present, the path MUST be prefixed by "/" + $path = '/' . $path; + } + } elseif (isset($path[1]) && '/' === $path[1]) { + if ('' === $authority) { + // If the path is starting with more than one "/" and no authority is present, the + // starting slashes MUST be reduced to one. + $path = '/' . \ltrim($path, '/'); + } + } + + $uri .= $path; + } + + if ('' !== $query) { + $uri .= '?' . $query; + } + + if ('' !== $fragment) { + $uri .= '#' . $fragment; + } + + return $uri; + } + + /** + * Is a given port non-standard for the current scheme? + */ + private static function isNonStandardPort(string $scheme, int $port): bool + { + return !isset(self::SCHEMES[$scheme]) || $port !== self::SCHEMES[$scheme]; + } + + private function filterPort($port): ?int + { + if (null === $port) { + return null; + } + + $port = (int) $port; + if (0 > $port || 0xFFFF < $port) { + throw new \InvalidArgumentException(\sprintf('Invalid port: %d. Must be between 0 and 65535', $port)); + } + + return self::isNonStandardPort($this->scheme, $port) ? $port : null; + } + + private function filterPath($path): string + { + if (!\is_string($path)) { + throw new \InvalidArgumentException('Path must be a string'); + } + + return \preg_replace_callback('/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/]++|%(?![A-Fa-f0-9]{2}))/', [__CLASS__, 'rawurlencodeMatchZero'], $path); + } + + private function filterQueryAndFragment($str): string + { + if (!\is_string($str)) { + throw new \InvalidArgumentException('Query and fragment must be a string'); + } + + return \preg_replace_callback('/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/', [__CLASS__, 'rawurlencodeMatchZero'], $str); + } + + private static function rawurlencodeMatchZero(array $match): string + { + return \rawurlencode($match[0]); + } +} diff --git a/vendor/phrity/comparison/composer.json b/vendor/phrity/comparison/composer.json new file mode 100644 index 000000000..99eb63833 --- /dev/null +++ b/vendor/phrity/comparison/composer.json @@ -0,0 +1,34 @@ +{ + "name": "phrity/comparison", + "type": "library", + "description": "Interfaces and helper trait for comparing objects. Comparator for sort and filter applications.", + "homepage": "https://phrity.sirn.se/comparison", + "keywords": ["comparison", "equalable", "comparable", "comparator", "sort", "filter"], + "license": "MIT", + "authors": [ + { + "name": "Sören Jensen", + "email": "sirn@sirn.se", + "homepage": "https://phrity.sirn.se" + } + ], + "autoload": { + "psr-4": { + "Phrity\\Comparison\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Mock\\": "tests/Mock/" + } + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", + "robiningelbrecht/phpunit-coverage-tools": "^1.9", + "squizlabs/php_codesniffer": "^3.5 || ^4.0" + } +} diff --git a/vendor/phrity/comparison/src/Comparable.php b/vendor/phrity/comparison/src/Comparable.php new file mode 100644 index 000000000..f84b0d4fd --- /dev/null +++ b/vendor/phrity/comparison/src/Comparable.php @@ -0,0 +1,56 @@ + Comparison + */ + +namespace Phrity\Comparison; + +/** + * Interface for comparable instances. + */ +interface Comparable extends Equalable +{ + /** + * Compare $this and $that and return result as comparison identifier as integer. + * @param mixed $that The instance to compare with + * @return integer Must return 0 if $this is equal to $that + * Must return -1 if $this is less than $that + * Must return 1 if $this is greater than $that + * @throws IncomparableException Must throw if $this can not be compared with $that + */ + public function compare(mixed $that): int; + + /** + * If $this is greater than $that. + * @param mixed $that The instance to compare with + * @return boolean True if $this is greater than $that + * @throws IncomparableException Must throw if $this can not be compared with $that + */ + public function greaterThan(mixed $that): bool; + + /** + * If $this is greater than or equal to $that. + * @param mixed $that The instance to compare with + * @return boolean True if $this is greater than or equal to $that + * @throws IncomparableException Must throw if $this can not be compared with $that + */ + public function greaterThanOrEqual(mixed $that): bool; + + /** + * If $this is less than $that. + * @param mixed $that The instance to compare with + * @return boolean True if $this is less than $that + * @throws IncomparableException Must throw if $this can not be compared with $that + */ + public function lessThan(mixed $that): bool; + + /** + * If $this is less than or equal to $this. + * @param mixed $that The instance to compare with + * @return boolean True if $this is less than or equal to $this + * @throws IncomparableException Must throw if $this can not be compared with $that + */ + public function lessThanOrEqual(mixed $that): bool; +} diff --git a/vendor/phrity/comparison/src/Comparator.php b/vendor/phrity/comparison/src/Comparator.php new file mode 100644 index 000000000..722253bb2 --- /dev/null +++ b/vendor/phrity/comparison/src/Comparator.php @@ -0,0 +1,200 @@ + Comparison + */ + +namespace Phrity\Comparison; + +/** + * Utility class for comparing and filtering. + */ +class Comparator +{ + /** @var array */ + private $comparables; + + /** + * If comparables supplied in constructor, they will be used as defaults an operations + * @param array $comparables List of objects implementing Comparable + */ + public function __construct(array $comparables = []) + { + $this->comparables = $comparables; + } + + // Sort methods + + /** + * Sorts array of comparable items, low to high + * @param array|null $comparables List of objects implementing Comparable + * @return array The sorted list + * @throws IncomparableException Thrown if any item in the list can not be compared + */ + public function sort(array|null $comparables = null): array + { + $comparables = $comparables ?: $this->comparables; + usort($comparables, function ($item_1, $item_2) { + $this->verifyComparable($item_1); + return $item_1->compare($item_2); + }); + return $comparables; + } + + /** + * Sorts array of comparable items, high to low + * @param array|null $comparables List of objects implementing Comparable + * @return array The sorted list + * @throws IncomparableException Thrown if any item in the list can not be compared + */ + public function rsort(array|null $comparables = null): array + { + $comparables = $comparables ?: $this->comparables; + usort($comparables, function ($item_1, $item_2) { + $this->verifyComparable($item_2); + return $item_2->compare($item_1); + }); + return $comparables; + } + + + // Filter methods + + /** + * Filter array of comparable items that equals condition + * @param Comparable $condition To compare against + * @param array|null $comparables List of objects implementing Comparable + * @return array The filtered list + * @throws IncomparableException Thrown if any item in the list can not be compared + */ + public function equals(Comparable $condition, array|null $comparables = null): array + { + return $this->applyFilter('equals', $condition, $comparables); + } + + /** + * Filter array of comparable items that are greater than condition + * @param Comparable $condition To compare against + * @param array|null $comparables List of objects implementing Comparable + * @return array The filtered list + * @throws IncomparableException Thrown if any item in the list can not be compared + */ + public function greaterThan(Comparable $condition, array|null $comparables = null): array + { + return $this->applyFilter('greaterThan', $condition, $comparables); + } + + /** + * Filter array of comparable items that are greater than or equals condition + * @param Comparable $condition To compare against + * @param array|null $comparables List of objects implementing Comparable + * @return array The filtered list + * @throws IncomparableException Thrown if any item in the list can not be compared + */ + public function greaterThanOrEqual(Comparable $condition, array|null $comparables = null): array + { + return $this->applyFilter('greaterThanOrEqual', $condition, $comparables); + } + + /** + * Filter array of comparable items that are less than condition + * @param Comparable $condition To compare against + * @param array|null $comparables List of objects implementing Comparable + * @return array The filtered list + * @throws IncomparableException Thrown if any item in the list can not be compared + */ + public function lessThan(Comparable $condition, array|null $comparables = null): array + { + return $this->applyFilter('lessThan', $condition, $comparables); + } + + /** + * Filter array of comparable items that are less than or equals condition + * @param Comparable $condition To compare against + * @param array|null $comparables List of objects implementing Comparable + * @return array The filtered list + * @throws IncomparableException Thrown if any item in the list can not be compared + */ + public function lessThanOrEqual(Comparable $condition, array|null $comparables = null): array + { + return $this->applyFilter('lessThanOrEqual', $condition, $comparables); + } + + + // Select methods + + /** + * Get minimum item from array of comparable items + * @param array|null $comparables List of objects implementing Comparable + * @return Comparable|null The resolved instance + * @throws IncomparableException Thrown if any item in the list can not be compared + */ + public function min(array|null $comparables = null): Comparable|null + { + return $this->applyReduction('lessThan', $comparables); + } + + /** + * Get maximum item from array of comparable items + * @param array|null $comparables List of objects implementing Comparable + * @return Comparable|null The resolved instance + * @throws IncomparableException Thrown if any item in the list can not be compared + */ + public function max(array|null $comparables = null): Comparable|null + { + return $this->applyReduction('greaterThan', $comparables); + } + + + // Private internal methods + + /** + * Verify input implements Comparable + * @param mixed $item Item to verify + * @throws IncomparableException Thrown if item do not implement Comparable + */ + private function verifyComparable(mixed $item): void + { + if (!$item instanceof Comparable) { + throw new IncomparableException('All items must implement Comparable'); + } + } + + /** + * Filter array of comparable items according to condition instance and method + * @param string $method Comparison method to use + * @param Comparable $condition To compare against + * @param array|null $comparables List of objects implementing Comparable + * @return array The filtered list + * @throws IncomparableException Thrown if any item in the list can not be compared + */ + private function applyFilter(string $method, Comparable $condition, array|null $comparables = null): array + { + $comparables = $comparables ?: $this->comparables; + $filtered = array_filter($comparables, function ($item) use ($method, $condition) { + $this->verifyComparable($item); + return $item->$method($condition); + }); + return array_values($filtered); + } + + /** + * Reduce array of comparable items according comparison method + * @param string $method Comparison method to use + * @param array|null $comparables List of objects implementing Comparable + * @return Comparable|null The resolved instance + * @throws IncomparableException Thrown if any item in the list can not be compared + */ + private function applyReduction(string $method, array|null $comparables = null): Comparable|null + { + $comparables = $comparables ?: $this->comparables; + return array_reduce($comparables, function ($item_1, $item_2) use ($method) { + if (is_null($item_1)) { + return $item_2; + } + $this->verifyComparable($item_1); + return $item_1->$method($item_2) ? $item_1 : $item_2; + }); + } +} diff --git a/vendor/phrity/comparison/src/ComparisonTrait.php b/vendor/phrity/comparison/src/ComparisonTrait.php new file mode 100644 index 000000000..dbea328b9 --- /dev/null +++ b/vendor/phrity/comparison/src/ComparisonTrait.php @@ -0,0 +1,69 @@ + Comparison + */ + +namespace Phrity\Comparison; + +/** + * Trait that enables comparison methods. + */ +trait ComparisonTrait +{ + /** + * If $this is equal to $that. + * @param mixed $that The instance to compare with + * @return boolean True if $this is equal to $that + * @throws IncomparableException Thrown if $this can not be compared with $that + */ + public function equals(mixed $that): bool + { + return $this->compare($that) == 0; + } + + /** + * If $this is greater than $that. + * @param mixed $that The instance to compare with + * @return boolean True if $this is greater than $that + * @throws IncomparableException Thrown if $this can not be compared with $that + */ + public function greaterThan(mixed $that): bool + { + return $this->compare($that) > 0; + } + + /** + * If $this is greater than or equal to $that. + * @param mixed $that The instance to compare with + * @return boolean True if $this is greater than or equal to $that + * @throws IncomparableException Thrown if $this can not be compared with $that + */ + public function greaterThanOrEqual(mixed $that): bool + { + return $this->compare($that) >= 0; + } + + /** + * If $this is less than $that. + * @param mixed $that The instance to compare with + * @return boolean True if $this is less than $that + * @throws IncomparableException Thrown if $this can not be compared with $that + */ + public function lessThan(mixed $that): bool + { + return $this->compare($that) < 0; + } + + /** + * If $this is less than or equal to $this. + * @param mixed $that The instance to compare with + * @return boolean True if $this is less than or equal to $this + * @throws IncomparableException Thrown if $this can not be compared with $that + */ + public function lessThanOrEqual(mixed $that): bool + { + return $this->compare($that) <= 0; + } +} diff --git a/vendor/phrity/comparison/src/Equalable.php b/vendor/phrity/comparison/src/Equalable.php new file mode 100644 index 000000000..8e207e390 --- /dev/null +++ b/vendor/phrity/comparison/src/Equalable.php @@ -0,0 +1,22 @@ + Comparison + */ + +namespace Phrity\Comparison; + +/** + * Interface for equalable instances. + */ +interface Equalable +{ + /** + * If $this is equal to $that. + * @param mixed $that The instance to compare with + * @return boolean True if $this is equal to $that + * @throws IncomparableException Must throw if $this can not be compared with $that + */ + public function equals(mixed $that): bool; +} diff --git a/vendor/phrity/comparison/src/IncomparableException.php b/vendor/phrity/comparison/src/IncomparableException.php new file mode 100644 index 000000000..fbc086f9f --- /dev/null +++ b/vendor/phrity/comparison/src/IncomparableException.php @@ -0,0 +1,17 @@ + Comparison + */ + +namespace Phrity\Comparison; + +use InvalidArgumentException; + +/** + * Exception that should be thrown when instances can not be compared. + */ +class IncomparableException extends InvalidArgumentException +{ +} diff --git a/vendor/phrity/http/LICENSE b/vendor/phrity/http/LICENSE new file mode 100644 index 000000000..09eaada6c --- /dev/null +++ b/vendor/phrity/http/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Sören Jensen + +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. diff --git a/vendor/phrity/http/composer.json b/vendor/phrity/http/composer.json new file mode 100644 index 000000000..d99b1ac3e --- /dev/null +++ b/vendor/phrity/http/composer.json @@ -0,0 +1,40 @@ +{ + "name": "phrity/http", + "type": "library", + "description": "Utilities and interfaces for handling HTTP.", + "homepage": "https://phrity.sirn.se/http", + "keywords": ["HTTP", "HTTP Factories", "HTTP Serializer", "PSR-7", "PSR-17"], + "license": "MIT", + "authors": [ + { + "name": "Sören Jensen", + "email": "sirn@sirn.se", + "homepage": "https://phrity.sirn.se" + } + ], + "autoload": { + "psr-4": { + "Phrity\\Http\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Phrity\\Http\\Test\\": "tests/" + } + }, + "require": { + "php": "^8.1", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0" + }, + "require-dev": { + "guzzlehttp/psr7": "^2.0", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", + "robiningelbrecht/phpunit-coverage-tools": "^1.9", + "squizlabs/php_codesniffer": "^3.5 || ^4.0" + }, + "provides": { + "psr/http-factory-implementation": "*" + } +} diff --git a/vendor/phrity/http/src/HttpFactory.php b/vendor/phrity/http/src/HttpFactory.php new file mode 100644 index 000000000..5e5ff6e92 --- /dev/null +++ b/vendor/phrity/http/src/HttpFactory.php @@ -0,0 +1,175 @@ +requestFactory)) { + throw new BadMethodCallException('HttpFactory.createRequest not implemented.'); + } + return $this->requestFactory->createRequest($method, $uri); + } + + /** + * @param int $code + * @param string $reasonPhrase + */ + public function createResponse(int $code = 200, string $reasonPhrase = ''): ResponseInterface + { + if (is_null($this->responseFactory)) { + throw new BadMethodCallException('HttpFactory.createResponse not implemented.'); + } + return $this->responseFactory->createResponse($code, $reasonPhrase); + } + + /** + * @param string $method + * @param UriInterface|string $uri + * @param array $serverParams + */ + public function createServerRequest(string $method, mixed $uri, array $serverParams = []): ServerRequestInterface + { + if (is_null($this->serverRequestFactory)) { + throw new BadMethodCallException('HttpFactory.createServerRequest not implemented.'); + } + return $this->serverRequestFactory->createServerRequest($method, $uri); + } + + /** + * @param string $content + */ + public function createStream(string $content = ''): StreamInterface + { + if (is_null($this->streamFactory)) { + throw new BadMethodCallException('HttpFactory.createStream not implemented.'); + } + return $this->streamFactory->createStream($content); + } + + /** + * @param string $filename + * @param string $mode + */ + public function createStreamFromFile(string $filename, string $mode = 'r'): StreamInterface + { + if (is_null($this->streamFactory)) { + throw new BadMethodCallException('HttpFactory.createStreamFromFile not implemented.'); + } + return $this->streamFactory->createStreamFromFile($filename, $mode); + } + + /** + * @param resource $resource + */ + public function createStreamFromResource($resource): StreamInterface + { + if (is_null($this->streamFactory)) { + throw new BadMethodCallException('HttpFactory.createStreamFromResource not implemented.'); + } + return $this->streamFactory->createStreamFromResource($resource); + } + + /** + * @param StreamInterface $stream + * @param int $size + * @param int $error + * @param string $clientFilename + * @param string $clientMediaType + */ + public function createUploadedFile( + StreamInterface $stream, + int|null $size = null, + int $error = UPLOAD_ERR_OK, + string|null $clientFilename = null, + string|null $clientMediaType = null + ): UploadedFileInterface { + if (is_null($this->uploadedFileFactory)) { + throw new BadMethodCallException('HttpFactory.createUploadedFile not implemented.'); + } + return $this->uploadedFileFactory->createUploadedFile( + $stream, + $size, + $error, + $clientFilename, + $clientMediaType + ); + } + + /** + * @param string $uri The URI to parse. + */ + public function createUri(string $uri = ''): UriInterface + { + if (is_null($this->uriFactory)) { + throw new BadMethodCallException('HttpFactory.createUri not implemented.'); + } + return $this->uriFactory->createUri($uri); + } + + public static function create(object ...$implementations): self + { + $created = new self(); + foreach ($implementations as $implementation) { + if ($implementation instanceof RequestFactoryInterface) { + $created->requestFactory = $implementation; + } + if ($implementation instanceof ResponseFactoryInterface) { + $created->responseFactory = $implementation; + } + if ($implementation instanceof ServerRequestFactoryInterface) { + $created->serverRequestFactory = $implementation; + } + if ($implementation instanceof StreamFactoryInterface) { + $created->streamFactory = $implementation; + } + if ($implementation instanceof UploadedFileFactoryInterface) { + $created->uploadedFileFactory = $implementation; + } + if ($implementation instanceof UriFactoryInterface) { + $created->uriFactory = $implementation; + } + } + return $created; + } +} diff --git a/vendor/phrity/http/src/Serializer.php b/vendor/phrity/http/src/Serializer.php new file mode 100644 index 000000000..8fa568891 --- /dev/null +++ b/vendor/phrity/http/src/Serializer.php @@ -0,0 +1,77 @@ +wrap( + '%s %s HTTP/%s', + $request->getMethod(), + $request->getRequestTarget(), + $request->getProtocolVersion() + ); + $headers = $this->formatHeaders($request); + $contents = $request->getBody()->getContents(); + return sprintf("%s%s\r\n%s", $status, $headers, $contents); + } + + /** + * @param ResponseInterface $response + */ + public function response(ResponseInterface $response): string + { + $status = $this->wrap( + 'HTTP/%s %s %s', + $response->getProtocolVersion(), + $response->getStatusCode(), + $response->getReasonPhrase() + ); + $headers = $this->formatHeaders($response); + $contents = $response->getBody()->getContents(); + return sprintf("%s%s\r\n%s", $status, $headers, $contents); + } + + /** + * @param MessageInterface $message + */ + public function message(MessageInterface $message): string + { + if ($message instanceof RequestInterface) { + return $this->request($message); + } elseif ($message instanceof ResponseInterface) { + return $this->response($message); + } + throw new DomainException(sprintf('Unsupported message type: %s', get_class($message))); + } + + protected function formatHeaders(MessageInterface $message): string + { + $lines = ''; + foreach ($message->getHeaders() as $name => $values) { + foreach ($values as $value) { + $lines .= $this->wrap('%s: %s', $name, $value); + } + } + return $lines; + } + + protected function wrap(string $format, string|int ...$values): string + { + return trim(sprintf($format, ...$values)) . "\r\n"; + } +} diff --git a/vendor/phrity/net-stream/LICENSE b/vendor/phrity/net-stream/LICENSE new file mode 100644 index 000000000..09eaada6c --- /dev/null +++ b/vendor/phrity/net-stream/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Sören Jensen + +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. diff --git a/vendor/phrity/net-stream/composer.json b/vendor/phrity/net-stream/composer.json new file mode 100644 index 000000000..5eb78d12a --- /dev/null +++ b/vendor/phrity/net-stream/composer.json @@ -0,0 +1,39 @@ +{ + "name": "phrity/net-stream", + "type": "library", + "description": "Socket stream classes implementing PSR-7 Stream and PSR-17 StreamFactory", + "homepage": "https://phrity.sirn.se/net-stream", + "keywords": ["socket", "stream", "stream factory", "client", "server", "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/" + } + }, + "autoload-dev": { + "psr-4": { + "Phrity\\Net\\Test\\": "tests/mock/" + } + }, + "require": { + "php": "^8.1", + "phrity/util-errorhandler": "^1.1", + "phrity/util-interpolator": "^1.1", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0 || ^13.0", + "phrity/net-uri": "^2.0", + "robiningelbrecht/phpunit-coverage-tools": "^1.9", + "squizlabs/php_codesniffer": "^4.0" + } +} diff --git a/vendor/phrity/net-stream/src/Context.php b/vendor/phrity/net-stream/src/Context.php new file mode 100644 index 000000000..4376c698a --- /dev/null +++ b/vendor/phrity/net-stream/src/Context.php @@ -0,0 +1,214 @@ +, Closure> */ + protected array $notifiers = []; + + /** + * Create exception. + * @param open-resource|null $stream + * @throws InvalidArgumentException if not a resource + * @throws InvalidArgumentException if wrong resource type + */ + public function __construct(mixed $stream = null) + { + if (is_null($stream)) { + $stream = stream_context_create(); + } + $type = gettype($stream); + if ($type !== 'resource') { + throw new InvalidArgumentException("Invalid stream provided; got type '{$type}'."); + } + $rtype = get_resource_type($stream); + if (!in_array($rtype, ['stream', 'persistent stream', 'stream-context'])) { + throw new InvalidArgumentException("Invalid stream provided; got resource type '{$rtype}'."); + } + $this->stream = $stream; + stream_context_set_params($this->stream, ['notification' => function (...$input) { + $this->notifyCallback(...$input); + }]); + } + + public function getOption(string $wrapper, string $option): mixed + { + return stream_context_get_options($this->stream)[$wrapper][$option] ?? null; + } + + /** + * @return array> + */ + public function getOptions(): array + { + return stream_context_get_options($this->stream); + } + + /** + * @throws StreamException on failure + */ + public function setOption(string $wrapper, string $option, mixed $value): self + { + if (!is_resource($this->stream) || !stream_context_set_option($this->stream, $wrapper, $option, $value)) { + throw new StreamException(StreamException::CONTEXT_SET_ERR); + } + return $this; + } + + /** + * @param array> $options + */ + public function setOptions(array $options): self + { + foreach ($options as $wrapper => $wrapperOptions) { + foreach ($wrapperOptions as $option => $value) { + $this->setOption($wrapper, $option, $value); + } + } + return $this; + } + + /** + * @deprecated Use getOption. + */ + public function getParam(string $param): mixed + { + return stream_context_get_params($this->stream)[$param] ?? null; + } + + /** + * @return array + * @deprecated Use getOptions. + */ + public function getParams(): array + { + return stream_context_get_params($this->stream); + } + + /** + * @deprecated Use setOption and on- callbacks instead. + */ + public function setParam(string $param, mixed $value): self + { + $this->setParams([$param => $value]); + return $this; + } + + /** + * @param array $params + * @deprecated Use setOptions and on- callbacks instead. + * @throws StreamException on failure + */ + public function setParams(array $params): self + { + /** @phpstan-ignore booleanNot.alwaysFalse */ + if (!is_resource($this->stream) || !stream_context_set_params($this->stream, $params)) { + throw new StreamException(StreamException::CONTEXT_SET_ERR); + } + return $this; + } + + public function getResource(): mixed + { + return $this->stream; + } + + /** @param Closure(): void $closure */ + public function onResolve(Closure $closure): void + { + $this->notifiers[STREAM_NOTIFY_RESOLVE] = $closure; + } + + /** @param Closure(): void $closure */ + public function onConnect(Closure $closure): void + { + $this->notifiers[STREAM_NOTIFY_CONNECT] = $closure; + } + + /** @param Closure(): void $closure */ + public function onAuthRequired(Closure $closure): void + { + $this->notifiers[STREAM_NOTIFY_AUTH_REQUIRED] = $closure; + } + + /** @param Closure(string $mimeType): void $closure */ + public function onMimeType(Closure $closure): void + { + $this->notifiers[STREAM_NOTIFY_MIME_TYPE_IS] = $closure; + } + + /** @param Closure(int $fileSize): void $closure */ + public function onFileSize(Closure $closure): void + { + $this->notifiers[STREAM_NOTIFY_FILE_SIZE_IS] = $closure; + } + + /** @param Closure(string $uri): void $closure */ + public function onRedirected(Closure $closure): void + { + $this->notifiers[STREAM_NOTIFY_REDIRECTED] = $closure; + } + + /** @param Closure(int $transferred, int $max): void $closure */ + public function onProgress(Closure $closure): void + { + $this->notifiers[STREAM_NOTIFY_PROGRESS] = $closure; + } + + /** @param Closure(): void $closure */ + public function onCompleted(Closure $closure): void + { + $this->notifiers[STREAM_NOTIFY_COMPLETED] = $closure; + } + + /** @param Closure(string $message, int $code): void $closure */ + public function onFailure(Closure $closure): void + { + $this->notifiers[STREAM_NOTIFY_FAILURE] = $closure; + } + + /** @param Closure(): void $closure */ + public function onAuthResult(Closure $closure): void + { + $this->notifiers[STREAM_NOTIFY_AUTH_RESULT] = $closure; + } + + protected function notifyCallback( + int $code, + int $severity, + string|null $message, + int $errorCode, + int $transferred, + int $max, + ): void { + if (!array_key_exists($code, $this->notifiers)) { + return; + } + $callback = $this->notifiers[$code]; + $params = match ($code) { + STREAM_NOTIFY_RESOLVE, + STREAM_NOTIFY_CONNECT, + STREAM_NOTIFY_AUTH_REQUIRED, + STREAM_NOTIFY_COMPLETED, + STREAM_NOTIFY_AUTH_RESULT => [], + STREAM_NOTIFY_MIME_TYPE_IS => ['mimeType' => $message], + STREAM_NOTIFY_FILE_SIZE_IS => ['fileSize' => $message], + STREAM_NOTIFY_REDIRECTED => ['uri' => $message], + STREAM_NOTIFY_PROGRESS => ['transferred' => $transferred, 'max' => $max], + STREAM_NOTIFY_FAILURE => ['message' => $message, 'code' => $errorCode], + }; + $callback(...$params); + } +} diff --git a/vendor/phrity/net-stream/src/SocketClient.php b/vendor/phrity/net-stream/src/SocketClient.php new file mode 100644 index 000000000..2a71c9132 --- /dev/null +++ b/vendor/phrity/net-stream/src/SocketClient.php @@ -0,0 +1,108 @@ +|float|null */ + protected int|float|null $timeout = null; + protected Context $context; + + /** + * Create new socker server instance + * @param UriInterface $uri The URI to open socket on. + */ + public function __construct(UriInterface $uri, Context|null $context = null) + { + $this->uri = $uri; + $this->context = $context ?? new Context(); + $this->handler = new ErrorHandler(); + } + + + // ---------- Configuration --------------------------------------------------------------------------------------- + + /** + * Set stream context. + * @param Context|array>|null $options + * @param array|null $params + * @return SocketClient + */ + public function setContext(Context|array|null $options = null, array|null $params = null): self + { + if ($options instanceof Context) { + $this->context = $options; + return $this; + } + // @deprecated + // @todo Add deprecation warning + $this->context->setOptions($options ?? []); + $this->context->setParams($params ?? []); + return $this; + } + + public function getContext(): Context + { + return $this->context; + } + + /** + * Set connection persistency. + * @param bool $persistent + * @return SocketClient + */ + public function setPersistent(bool $persistent): self + { + $this->persistent = $persistent; + return $this; + } + + /** + * Set timeout in seconds. + * @param int<0, max>|float|null $timeout + * @return SocketClient + * @throws InvalidArgumentException if invalid timeout + */ + public function setTimeout(int|float|null $timeout): self + { + if (!is_null($timeout) && $timeout < 0) { + throw new InvalidArgumentException("Timeout must be 0 or more."); + } + $this->timeout = $timeout; + return $this; + } + + + // ---------- Operations ------------------------------------------------------------------------------------------ + + /** + * Create a connection on remote socket. + * @return SocketStream The stream for opened conenction. + */ + public function connect(): SocketStream + { + /** @throws StreamException if connection could not be created */ + $stream = $this->handler->with(function () { + $error_code = $error_message = ''; + return stream_socket_client( + $this->uri->__toString(), + $error_code, + $error_message, + $this->timeout, + $this->persistent ? STREAM_CLIENT_CONNECT | STREAM_CLIENT_PERSISTENT : STREAM_CLIENT_CONNECT, + $this->context->getResource() + ); + }, new StreamException(StreamException::CLIENT_CONNECT_ERR, ['uri' => $this->uri])); + return new SocketStream($stream); + } +} diff --git a/vendor/phrity/net-stream/src/SocketServer.php b/vendor/phrity/net-stream/src/SocketServer.php new file mode 100644 index 000000000..217d08877 --- /dev/null +++ b/vendor/phrity/net-stream/src/SocketServer.php @@ -0,0 +1,173 @@ + */ + private static array $internet_schemes = ['tcp', 'udp', 'tls', 'ssl']; + /** @var array */ + private static array $unix_schemes = ['unix', 'udg']; + + protected ErrorHandler $handler; + protected string $address; + /** @var resource */ + protected $stream; + protected Context $context; + + /** + * Create new socker server instance + * @param UriInterface $uri The URI to open socket on. + * @throws StreamException if invalid scheme. + * @throws StreamException if unsupported scheme. + * @throws StreamException if unable to create socket. + */ + public function __construct(UriInterface $uri, Context|null $context = null) + { + $this->handler = new ErrorHandler(); + if (!in_array($uri->getScheme(), $this->getTransports())) { + throw new StreamException(StreamException::SCHEME_TRANSPORT, ['scheme' => $uri->getScheme()]); + } + if (in_array(substr($uri->getScheme(), 0, 3), self::$internet_schemes)) { + $this->address = "{$uri->getScheme()}://{$uri->getAuthority()}"; + } elseif (in_array($uri->getScheme(), self::$unix_schemes)) { + $this->address = "{$uri->getScheme()}://{$uri->getPath()}"; + } else { + throw new StreamException(StreamException::SCHEME_HANDLER, ['scheme' => $uri->getScheme()]); + } + $this->context = $context ?? new Context(); + /** @throws StreamException on failure */ + $this->stream = $this->handler->with(function () { + $error_code = $error_message = ''; + return stream_socket_server( + $this->address, + $error_code, + $error_message, + STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, + $this->context->getResource() + ); + }, new StreamException(StreamException::SERVER_SOCKET_ERR, ['uri' => $uri->__toString()])); + $this->evalStream(); + } + + + // ---------- Configuration --------------------------------------------------------------------------------------- + + /** + * Set stream context. + * @param Context|array>|null $options + * @param array|null $params + * @return static + */ + public function setContext(Context|array|null $options = null, array|null $params = null): self + { + if ($options instanceof Context) { + $this->context = $options; + return $this; + } + // @deprecated + // @todo Add deprecation warning + $this->context->setOptions($options ?? []); + $this->context->setParams($params ?? []); + return $this; + } + + public function getContext(): Context + { + return $this->context; + } + + /** + * Retrieve list of registered socket transports. + * @return array List of registered transports. + */ + public function getTransports(): array + { + return stream_get_transports(); + } + + /** + * If server is in blocking mode. + * @return bool|null + */ + public function isBlocking(): bool|null + { + return $this->getMetadata('blocked'); + } + + /** + * Toggle blocking/non-blocking mode. + * @param bool $enable Blocking mode to set. + * @return bool If operation was succesful. + * @throws StreamException if socket is closed. + */ + public function setBlocking(bool $enable): bool + { + if (!is_resource($this->stream)) { + throw new StreamException(StreamException::SERVER_CLOSED); + } + return stream_set_blocking($this->stream, $enable); + } + + /** + * Get stream metadata as an associative array or retrieve a specific key. + * @param string $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|null $key = null): mixed + { + if (!is_resource($this->stream)) { + return null; + } + // Add URI default for version compability + $meta = array_merge([ + 'uri' => $this->address, + ], stream_get_meta_data($this->stream)); + if (isset($key)) { + return array_key_exists($key, $meta) ? $meta[$key] : null; + } + return $meta; + } + + + // ---------- Operations ------------------------------------------------------------------------------------------ + + /** + * Accept a connection on a socket. + * @param int<0, max>|float|null $timeout Override the default socket accept timeout. + * @return SocketStream|null The stream for opened conenction. + * @throws InvalidArgumentException if invalid timeout + * @throws StreamException if socket is closed + */ + public function accept(int|float|null $timeout = null): SocketStream|null + { + if (!is_null($timeout) && $timeout < 0) { + throw new InvalidArgumentException("Timeout must be 0 or more."); + } + if (!is_resource($this->stream)) { + throw new StreamException(StreamException::SERVER_CLOSED); + } + /** @throws StreamException */ + $stream = $this->handler->with(function () use ($timeout) { + $peer_name = ''; + return stream_socket_accept($this->stream, $timeout, $peer_name); + }, function (ErrorException $e) { + // If non-blocking mode, don't throw error on time out + if ($this->getMetadata('blocked') === false && substr_count($e->getMessage(), 'timed out') > 0) { + return null; + } + throw new StreamException(StreamException::SERVER_ACCEPT_ERR, [], $e); + }); + return $stream ? new SocketStream($stream) : null; + } +} diff --git a/vendor/phrity/net-stream/src/SocketStream.php b/vendor/phrity/net-stream/src/SocketStream.php new file mode 100644 index 000000000..d83dc98c6 --- /dev/null +++ b/vendor/phrity/net-stream/src/SocketStream.php @@ -0,0 +1,166 @@ +stream) && ($this->readable || $this->writable); + } + + /** + * Get name of remote socket, or null if not connected. + * @return string|null + */ + public function getRemoteName(): string|null + { + return is_resource($this->stream) ? (stream_socket_get_name($this->stream, true) ?: null) : null; + } + + /** + * Get name of local socket, or null if not connected. + * @return string|null + */ + public function getLocalName(): string|null + { + return is_resource($this->stream) ? (stream_socket_get_name($this->stream, false) ?: null) : null; + } + + /** + * Get type of stream resoucre. + * @return string + */ + public function getResourceType(): string + { + return $this->stream ? get_resource_type($this->stream) : ''; + } + + /** + * If stream is in blocking mode. + * @return bool|null + */ + public function isBlocking(): bool|null + { + return $this->getMetadata('blocked'); + } + + /** + * Toggle blocking/non-blocking mode. + * @param bool $enable Blocking mode to set. + * @return bool If operation was succesful. + * @throws StreamException if stream is closed. + */ + public function setBlocking(bool $enable): bool + { + if (!isset($this->stream)) { + throw new StreamException(StreamException::STREAM_DETACHED); + } + return stream_set_blocking($this->stream, $enable); + } + + /** + * If socket stream has unread content. + * @return bool If there is content to read. + * @throws StreamException if stream is unselectable. + */ + public function hasContents(): bool + { + if (!is_resource($this->stream)) { + return false; + } + /** @throws StreamException */ + return $this->handler->with(function () { + $read = [$this->getOpenResource()]; + $write = $oob = []; + return stream_select($read, $write, $oob, 0, 0) > 0; + }, new StreamException(StreamException::FAIL_SELECT)); + } + + /** + * Set timeout period on a stream. + * @param int<0, max>|float $timeout Seconds to be set. + * @param int|null $microseconds Microseconds to be set - deprecated + * @return bool If operation was succesful. + * @throws InvalidArgumentException if invalid timeout. + * @throws StreamException if stream is closed. + */ + public function setTimeout(int|float $timeout, int|null $microseconds = null): bool + { + // @deprecated Setting $microseconds is deprecated, use float value on $timeout instead + // @todo Add deprecation warning + if ($timeout < 0) { + throw new InvalidArgumentException("Timeout must be 0 or more."); + } + if (!isset($this->stream)) { + throw new StreamException(StreamException::STREAM_DETACHED); + } + $seconds = intval($timeout); + $microseconds = $microseconds ?? intval(round($timeout - $seconds, 6) * 1000000); + return stream_set_timeout($this->stream, $seconds, $microseconds); + } + + + // ---------- Operations ------------------------------------------------------------------------------------------ + + /** + * Read line from the stream. + * @param int<0, max> $length Read up to $length bytes from the object and return them. + * @return string|null Returns the data read from the stream, or null of eof. + * @throws StreamException if an error occurs. + */ + public function readLine(int $length): string|null + { + $stream = $this->getOpenResource(); + if (!$this->readable) { + throw new StreamException(StreamException::NOT_READABLE); + } + /** @throws StreamException */ + return $this->handler->with(function () use ($stream, $length) { + $result = fgets($stream, $length); + return $result === false ? null : $result; + }, new StreamException(StreamException::FAIL_GETS)); + } + + /** + * Closes the stream for further reading. + * @return void + */ + public function closeRead(): void + { + if (is_resource($this->stream)) { + if ($this->readable && $this->writable) { + stream_socket_shutdown($this->stream, STREAM_SHUT_RD); + $this->evalStream(); + } elseif (!$this->writable) { + $this->close(); + } + } + $this->readable = false; + } + /** + * Closes the stream for further writing. + * @return void + */ + public function closeWrite(): void + { + if ($this->readable && $this->writable) { + stream_socket_shutdown($this->getOpenResource(), STREAM_SHUT_WR); + $this->evalStream(); + } elseif (!$this->readable) { + $this->close(); + } + $this->writable = false; + } +} diff --git a/vendor/phrity/net-stream/src/Stream.php b/vendor/phrity/net-stream/src/Stream.php new file mode 100644 index 000000000..c4fbda3da --- /dev/null +++ b/vendor/phrity/net-stream/src/Stream.php @@ -0,0 +1,316 @@ + */ + private static array $readmodes = ['r', 'r+', 'w+', 'a+', 'x+', 'c+']; + /** @var array */ + private static array $writemodes = ['r+', 'w', 'w+', 'a', 'a+', 'x', 'x+', 'c', 'c+']; + + /** @var resource|null */ + protected $stream; + protected Context $context; + protected ErrorHandler $handler; + protected bool $readable = false; + protected bool $writable = false; + protected bool $seekable = false; + + /** + * Create new stream wrapper instance + * @param resource $stream A stream resource to wrap + * @throws InvalidArgumentException If not a valid stream resource + */ + public function __construct($stream) + { + $type = gettype($stream); + if ($type !== 'resource') { + throw new InvalidArgumentException("Invalid stream provided; got type '{$type}'."); + } + $rtype = get_resource_type($stream); + if (!in_array($rtype, ['stream', 'persistent stream'])) { + throw new InvalidArgumentException("Invalid stream provided; got resource type '{$rtype}'."); + } + $this->stream = $stream; + $this->context = new Context($this->stream); + $this->handler = new ErrorHandler(); + $this->evalStream(); + } + + + // ---------- PSR-7 methods --------------------------------------------------------------------------------------- + + /** + * Closes the stream and any underlying resources. + * @return void + */ + public function close(): void + { + if (is_resource($this->stream)) { + fclose($this->stream); + } + $this->stream = null; + $this->evalStream(); + } + + /** + * Separates any underlying resources from the stream. + * After the stream has been detached, the stream is in an unusable state. + * @return resource|null Underlying stream, if any + */ + public function detach(): mixed + { + if (!isset($this->stream)) { + return null; + } + $stream = $this->stream; + $this->stream = null; + $this->evalStream(); + return $stream; + } + + /** + * Get stream metadata as an associative array or retrieve a specific key. + * @param string $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|null $key = null): mixed + { + if (!isset($this->stream)) { + return null; + } + $meta = stream_get_meta_data($this->stream); + if (isset($key)) { + return array_key_exists($key, $meta) ? $meta[$key] : null; + } + return $meta; + } + + /** + * Returns the current position of the file read/write pointer + * @return int Position of the file pointer + * @throws StreamException on error. + */ + public function tell(): int + { + /** @throws StreamException */ + return $this->handler->with(function () { + return ftell($this->getOpenResource()); + }, new StreamException(StreamException::FAIL_TELL)); + } + + /** + * Returns true if the stream is at the end of the stream. + * @return bool + */ + public function eof(): bool + { + return empty($this->stream) || feof($this->stream); + } + + /** + * Read data from the stream. + * @param int<1, max> $length Read up to $length bytes from the object and return them. + * @return string Returns the data read from the stream, or an empty string. + * @throws StreamException if an error occurs. + */ + public function read(int $length): string + { + if ($length < 1) { + throw new InvalidArgumentException("Must read minimum 1 byte"); + } + $stream = $this->getOpenResource(); + if (!$this->readable) { + throw new StreamException(StreamException::NOT_READABLE); + } + /** @throws StreamException */ + return $this->handler->with(function () use ($stream, $length) { + return (string)fread($stream, $length); + }, new StreamException(StreamException::FAIL_READ)); + } + + /** + * 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 StreamException on failure. + */ + public function write(string $string): int + { + $stream = $this->getOpenResource(); + if (!$this->writable) { + throw new StreamException(StreamException::NOT_WRITABLE); + } + /** @throws StreamException */ + return $this->handler->with(function () use ($stream, $string) { + return fwrite($stream, $string); + }, new StreamException(StreamException::FAIL_WRITE)); + } + + /** + * 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(): int|null + { + if (!is_resource($this->stream)) { + return null; + } + $stats = fstat($this->stream); + return $stats['size'] ?? null; + } + + /** + * Returns whether or not the stream is seekable. + * @return bool + */ + public function isSeekable(): bool + { + return $this->seekable; + } + + /** + * Seek to a position in the stream. + * @param int $offset Stream offset + * @param int $whence Specifies how the cursor position will be calculated based on the seek offset. + * @throws StreamException on failure. + */ + public function seek(int $offset, int $whence = SEEK_SET): void + { + $stream = $this->getOpenResource(); + if (!$this->seekable) { + throw new StreamException(StreamException::NOT_SEEKABLE); + } + $result = fseek($stream, $offset, $whence); + if ($result !== 0) { + throw new StreamException(StreamException::FAIL_SEEK); + } + } + + /** + * 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). + */ + public function rewind(): void + { + $this->seek(0); + } + + /** + * Returns whether or not the stream is writable. + * @return bool + */ + public function isWritable(): bool + { + return $this->writable; + } + + /** + * Returns whether or not the stream is readable. + * @return bool + */ + public function isReadable(): bool + { + return $this->readable; + } + + /** + * Returns the remaining contents in a string + * @return string + * @throws StreamException if unable to read. + * @throws StreamException if error occurs while reading. + */ + public function getContents(): string + { + $stream = $this->getOpenResource(); + if (!$this->readable) { + throw new StreamException(StreamException::NOT_READABLE); + } + /** @throws StreamException */ + return $this->handler->with(function () use ($stream) { + return stream_get_contents($stream); + }, new StreamException(StreamException::FAIL_CONTENTS)); + } + + /** + * Reads all data from the stream into a string, from the beginning to end. + * @return string + */ + public function __toString(): string + { + try { + if ($this->isSeekable()) { + $this->rewind(); + } + return $this->getContents(); + } catch (Throwable $e) { + trigger_error($e->getMessage(), E_USER_WARNING); + return ''; + } + } + + + // ---------- StreamInterface methods ----------------------------------------------------------------------------- + + /** + * Return context for stream. + * @return Context + */ + public function getContext(): Context + { + return $this->context; + } + + /** + * Return underlying resource. + * @return resource|null + */ + public function getResource(): mixed + { + return $this->stream; + } + + + // ---------- Protected helper methods ---------------------------------------------------------------------------- + + /** + * Evaluate stream state. + */ + protected function evalStream(): void + { + if ($this->stream && $meta = $this->getMetadata()) { + $mode = substr($meta['mode'], 0, 2); + $this->readable = in_array($mode, self::$readmodes); + $this->writable = in_array($mode, self::$writemodes); + $this->seekable = $meta['seekable']; + return; + } + $this->readable = $this->writable = $this->seekable = false; + } + + /** + * Return underlying resource. + * @return resource + * @throws StreamException if closed. + */ + protected function getOpenResource(): mixed + { + if (!is_resource($this->stream)) { + throw new StreamException(StreamException::STREAM_DETACHED); + } + return $this->stream; + } +} diff --git a/vendor/phrity/net-stream/src/StreamCollection.php b/vendor/phrity/net-stream/src/StreamCollection.php new file mode 100644 index 000000000..941dce6e0 --- /dev/null +++ b/vendor/phrity/net-stream/src/StreamCollection.php @@ -0,0 +1,220 @@ + + */ +class StreamCollection implements Countable, Iterator +{ + protected ErrorHandler $handler; + /** @var array */ + private array $streams = []; + + /** + * Create new stream collection instance. + */ + public function __construct() + { + $this->handler = new ErrorHandler(); + } + + + // ---------- Collectors and selectors ---------------------------------------------------------------------------- + + /** + * Attach stream to collection. + * @param StreamInterface|StreamContainerInterface $attach Stream to attach. + * @param string|null $key Definable name of stream. + * @return string Name of stream. + * @throws StreamException If already attached. + */ + public function attach(StreamInterface|StreamContainerInterface $attach, string|null $key = null): string + { + if ($key && array_key_exists($key, $this->streams)) { + throw new StreamException(StreamException::COLLECT_KEY_CONFLICT, ['key' => $key]); + } + $key = $key ?: $this->createKey(); + $this->streams[$key] = $attach; + return $key; + } + + /** + * Detach stream from collection. + * @param StreamInterface|StreamContainerInterface|string $detach Stream or name of stream to detach. + * @return bool If a stream was detached. + */ + public function detach(StreamInterface|StreamContainerInterface|string $detach): bool + { + if (is_string($detach)) { + if (array_key_exists($detach, $this->streams)) { + unset($this->streams[$detach]); + return true; + } + } + if ($detach instanceof Stream) { + foreach ($this->streams as $key => $stream) { + if ($stream === $detach) { + unset($this->streams[$key]); + return true; + } + } + } + return false; + } + + /** + * Collect all readable streams into new collection. + * @return self New collection instance. + */ + public function getReadable(): self + { + $readables = new self(); + foreach ($this->streams as $key => $stream) { + if ($this->getStream($stream)->isReadable()) { + $readables->attach($stream, $key); + } + } + return $readables; + } + + /** + * Collect all writable streams into new collection. + * @return self New collection instance. + */ + public function getWritable(): self + { + $writables = new self(); + foreach ($this->streams as $key => $stream) { + if ($this->getStream($stream)->isWritable()) { + $writables->attach($stream, $key); + } + } + return $writables; + } + + /** + * Wait for redable content in stream collection. + * @param int<0, max>|float $timeout Timeout in seconds. + * @return self New collection instance. + * @throws InvalidArgumentException If invalid timeout. + */ + public function waitRead(int|float $timeout = 60): self + { + if ($timeout < 0) { + throw new InvalidArgumentException("Timeout must be 0 or more."); + } + $seconds = intval($timeout); + $microseconds = intval(round($timeout - $seconds, 6) * 1000000); + + $read = []; + foreach ($this->streams as $key => $stream) { + if ($this->getStream($stream)->isReadable()) { + $read[$key] = $this->getStream($stream)->getResource(); + } + } + if (empty($read)) { + return new self(); // Nothing to select + } + + $changed = $this->handler->with(function () use ($read, $seconds, $microseconds) { + $write = $oob = []; + /** @phpstan-ignore argument.type */ + stream_select($read, $write, $oob, $seconds, $microseconds); + return $read; + }, function (ErrorException $error) { + return []; // Ignore, but don't use result + }); + + $ready = new self(); + foreach ($changed as $key => $resource) { + $ready->attach($this->streams[$key], $key); + } + return $ready; + } + + + // ---------- Countable interface implementation ------------------------------------------------------------------ + + /** + * Count contained streams. + * @return int Number of streams in collection. + */ + public function count(): int + { + return count($this->streams); + } + + + // ---------- Iterator interface implementation ------------------------------------------------------------------- + + /** + * Return the current stream. + * @return StreamInterface|StreamContainerInterface|null Current stream. + */ + public function current(): StreamInterface|StreamContainerInterface|null + { + return current($this->streams) ?: null; + } + + /** + * Return the key of the current stream. + * @return string Current key. + */ + public function key(): string|null + { + return key($this->streams); + } + + /** + * Move forward to next stream. + */ + public function next(): void + { + next($this->streams); + } + + /** + * Rewind the Iterator to the first stream. + */ + public function rewind(): void + { + reset($this->streams); + } + + /** + * Checks if current position is valid. + * @return bool True if valid. + */ + public function valid(): bool + { + return array_key_exists(key($this->streams) ?? -1, $this->streams); + } + + + // ---------- Protected helper methods ---------------------------------------------------------------------------- + + /** + * Create unique key. + * @return string Unique key. + */ + protected function createKey(): string + { + do { + $key = bin2hex(random_bytes(16)); + } while (array_key_exists($key, $this->streams)); + return $key; + } + + protected function getStream(StreamInterface|StreamContainerInterface $stream): StreamInterface + { + return $stream instanceof StreamInterface ? $stream : $stream->getStream(); + } +} diff --git a/vendor/phrity/net-stream/src/StreamContainerInterface.php b/vendor/phrity/net-stream/src/StreamContainerInterface.php new file mode 100644 index 000000000..71316662a --- /dev/null +++ b/vendor/phrity/net-stream/src/StreamContainerInterface.php @@ -0,0 +1,14 @@ + */ + private static array $messages = [ + self::STREAM_DETACHED => 'Stream is detached.', + self::NOT_READABLE => 'Stream is not readable.', + self::NOT_WRITABLE => 'Stream is not writable.', + self::NOT_SEEKABLE => 'Stream is not seekable.', + self::FAIL_READ => 'Failed read() on stream.', + self::FAIL_WRITE => 'Failed write() on stream.', + self::FAIL_SEEK => 'Failed seek() on stream.', + self::FAIL_TELL => 'Failed tell() on stream.', + self::FAIL_CONTENTS => 'Failed getContents() on stream.', + self::FAIL_GETS => 'Failed gets() on stream.', + self::FAIL_SELECT => 'Failed select() on stream.', + self::CLIENT_CONNECT_ERR => 'Client could not connect to "{uri}".', + self::SCHEME_TRANSPORT => 'Scheme "{scheme}" is not supported.', + self::SCHEME_HANDLER => 'Could not handle scheme "{scheme}".', + self::SERVER_SOCKET_ERR => 'Could not create socket for "{uri}".', + self::SERVER_CLOSED => 'Server is closed.', + self::SERVER_ACCEPT_ERR => 'Could not accept on socket.', + self::COLLECT_KEY_CONFLICT => 'Stream with name "{key}" already attached.', + self::COLLECT_SELECT_ERR => 'Failed to select streams for reading.', + self::CONTEXT_SET_ERR => 'Failed to set option/param on context.', + ]; + + /** + * Create exception. + * @param int $code Error code + * @param array $data Additional data + * @param Throwable|null $previous Previous exception + */ + public function __construct(int $code, array $data = [], Throwable|null $previous = null) + { + $message = self::$messages[$code]; + $message = $this->interpolate($message, $data); + if ($previous) { + $message .= " ({$previous->getMessage()})"; + } + parent::__construct($message, $code, $previous); + } +} diff --git a/vendor/phrity/net-stream/src/StreamFactory.php b/vendor/phrity/net-stream/src/StreamFactory.php new file mode 100644 index 000000000..d37ed3288 --- /dev/null +++ b/vendor/phrity/net-stream/src/StreamFactory.php @@ -0,0 +1,133 @@ + */ + private static array $modes = ['r', 'r+', 'w', 'w+', 'a', 'a+', 'x', 'x+', 'c', 'c+', 'e']; + + private ErrorHandler $handler; + + /** + * Create new stream wrapper instance. + */ + public function __construct() + { + $this->handler = new ErrorHandler(); + } + + + // ---------- PSR-17 methods -------------------------------------------------------------------------------------- + + /** + * Create a new stream from a string. + * @param string $content String content with which to populate the stream. + * @return Stream A stream instance. + */ + public function createStream(string $content = ''): Stream + { + $resource = $this->createResource('php://temp', 'r+'); + fwrite($resource, $content); + return $this->createStreamFromResource($resource); + } + + /** + * Create a stream from an existing file. + * @param string $filename The filename or stream URI to use as basis of stream. + * @param string $mode The mode with which to open the underlying filename/stream. + * @throws InvalidArgumentException If the mode is invalid. + * @return Stream A stream instance. + */ + public function createStreamFromFile(string $filename, string $mode = 'r'): Stream + { + if (!in_array($mode, self::$modes)) { + throw new InvalidArgumentException("Invalid mode '{$mode}'."); + } + $resource = $this->createResource($filename, $mode); + return $this->createStreamFromResource($resource); + } + + /** + * Create a new stream from an existing resource. + * The stream MUST be readable and may be writable. + * @param resource $resource The PHP resource to use as the basis for the stream. + * @return Stream A stream instance. + */ + public function createStreamFromResource($resource): Stream + { + return new Stream($resource); + } + + + // ---------- Extensions ------------------------------------------------------------------------------------------ + + /** + * Create a new socket client. + * @param UriInterface $uri The URI to connect to. + * @return SocketClient A socket client instance. + */ + public function createSocketClient(UriInterface $uri, Context|null $context = null): SocketClient + { + return new SocketClient($uri, $context); + } + + /** + * Create a new socket server. + * @param UriInterface $uri The URI to create server on. + * @return SocketServer A socket server instance. + */ + public function createSocketServer(UriInterface $uri, Context|null $context = null): SocketServer + { + return new SocketServer($uri, $context); + } + + /** + * Create a new ocket stream from an existing resource. + * The stream MUST be readable and may be writable. + * @param resource $resource The PHP resource to use as the basis for the stream. + * @return SocketStream A socket stream instance. + */ + public function createSocketStreamFromResource($resource): SocketStream + { + return new SocketStream($resource); + } + + /** + * Create a new stream collection. + * @return StreamCollection A stream collection. + */ + public function createStreamCollection(): StreamCollection + { + return new StreamCollection(); + } + + + // ---------- Helpers --------------------------------------------------------------------------------------------- + + /** + * @return resource + * @throws RuntimeException If fails to open resource + */ + private function createResource(string $filename, string $mode) + { + /** @throws RuntimeException */ + return $this->handler->with(function () use ($filename, $mode) { + /** @var resource $resource */ + $resource = fopen($filename, $mode); + return $resource; + }, new RuntimeException("Could not open '{$filename}'.")); + } +} diff --git a/vendor/phrity/net-stream/src/StreamInterface.php b/vendor/phrity/net-stream/src/StreamInterface.php new file mode 100644 index 000000000..f280ff96c --- /dev/null +++ b/vendor/phrity/net-stream/src/StreamInterface.php @@ -0,0 +1,21 @@ + 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 JsonSerializable; +use Phrity\Comparison\{ + Equalable, + IncomparableException, +}; +use Psr\Http\Message\UriInterface; +use Stringable; + +/** + * Net\Uri class. + */ +class Uri implements Equalable, JsonSerializable, Stringable, 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; // @deprecated, replaced by IDN_ENCODE + public const IDN_ENCODE = 16; // IDN-encode host + public const IDN_DECODE = 32; // IDN-decode host + public const URI_DECODE = 64; // Decoded URI + public const URI_ENCODE = 128; // Minimal URI encoded + public const URI_ENCODE_3986 = 256; // URI encoded RFC 3986 + + private const RE_MAIN = '!^(?P(?P[^:/?#]+):)?(?P//(?P[^/?#]*))?' + . '(?P[^?#]*)(?P\?(?P[^#]*))?(?P#(?P.*))?$!'; + private const RE_AUTH = '!^(?P(?P[^:/?#]+)(?P:(?P[^:/?#]+))?@)?' + . '(?P[^:/?#]*|\[[^/?#]*\])(?P:(?P[0-9]*))?$!'; + + /** @var array $portDefaults */ + private static array $portDefaults = [ + '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 string $scheme = ''; + private bool $authority = false; + private string $host = ''; + private int|null $port = null; + private string $user = ''; + private string|null $pass = null; + private string $path = ''; + private string $query = ''; + private string $fragment = ''; + + /** + * Create new URI instance using a string + * @param string $uriString URI as string + * @throws InvalidArgumentException If the given URI cannot be parsed + */ + public function __construct(string $uriString = '') + { + $this->parse($uriString); + } + + + // ---------- PSR-7 getters --------------------------------------------------------------------------------------- + + /** + * Retrieve the scheme component of the URI. + * @param int $flags Optional modifier flags + * @return string The URI scheme + */ + public function getScheme(int $flags = 0): string + { + return $this->scheme; + } + + /** + * Retrieve the authority component of the URI. + * @param int $flags Optional modifier flags + * @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 ($host === '') { + return ''; + } + $userinfo = $this->formatComponent($this->getUserInfo($flags), '', '@'); + $port = $this->formatComponent($this->getPort($flags), ':'); + return "{$userinfo}{$host}{$port}"; + } + + /** + * Retrieve the user information component of the URI. + * @param int $flags Optional modifier flags + * @return string The URI user information, in "username[:password]" format + */ + public function getUserInfo(int $flags = 0): string + { + $user = $this->formatComponent($this->uriEncode($this->user, $flags)); + $pass = $this->formatComponent($this->uriEncode($this->pass ?? '', $flags), ':'); + return $user === '' ? '' : "{$user}{$pass}"; + } + + /** + * Retrieve the host component of the URI. + * @param int $flags Optional modifier flags + * @return string The URI host + */ + public function getHost(int $flags = 0): string + { + if ($flags & self::IDNA) { + trigger_error("Flag IDNA is deprecated; use IDN_ENCODE instead", E_USER_DEPRECATED); + return $this->idnEncode($this->host); + } + if ($flags & self::IDN_ENCODE) { + return $this->idnEncode($this->host); + } + if ($flags & self::IDN_DECODE) { + return $this->idnDecode($this->host); + } + return $this->host; + } + + /** + * Retrieve the port component of the URI. + * @param int $flags Optional modifier flags + * @return null|int The URI port + */ + public function getPort(int $flags = 0): int|null + { + $default = self::$portDefaults[$this->scheme] ?? null; + if ($flags & self::REQUIRE_PORT) { + return $this->port !== null ? $this->port : $default; + } + return $this->port === $default ? null : $this->port; + } + + /** + * Retrieve the path component of the URI. + * @param int $flags Optional modifier flags + * @return string The URI path + */ + public function getPath(int $flags = 0): string + { + $path = $this->path; + if ($flags & self::NORMALIZE_PATH) { + $path = $this->normalizePath($path); + } + if ($flags & self::ABSOLUTE_PATH && substr($path, 0, 1) !== '/') { + $path = "/{$path}"; + } + return $this->uriEncode($path, $flags, '\/:@'); + } + + /** + * Retrieve the query string of the URI. + * @param int $flags Optional modifier flags + * @return string The URI query string + */ + public function getQuery(int $flags = 0): string + { + return $this->uriEncode($this->query, $flags, '\/:@?'); + } + + /** + * Retrieve the fragment component of the URI. + * @param int $flags Optional modifier flags + * @return string The URI fragment + */ + public function getFragment(int $flags = 0): string + { + return $this->uriEncode($this->fragment, $flags, '\/:@?'); + } + + + // ---------- PSR-7 setters --------------------------------------------------------------------------------------- + + /** + * Return an instance with the specified scheme. + * @param string $scheme The scheme to use with the new instance + * @param int $flags Optional modifier flags + * @return self A new instance with the specified scheme + * @throws InvalidArgumentException for invalid schemes + * @throws InvalidArgumentException for unsupported schemes + */ + public function withScheme(string $scheme, int $flags = 0): self + { + $clone = $this->clone($flags); + $clone->setScheme($scheme, $flags); + 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 + * @param int $flags Optional modifier flags + * @return self A new instance with the specified user information + */ + public function withUserInfo(string $user, string|null $password = null, int $flags = 0): self + { + $clone = $this->clone($flags); + $clone->setUserInfo($user, $password); + return $clone; + } + + /** + * Return an instance with the specified host. + * @param string $host The hostname to use with the new instance + * @param int $flags Optional modifier flags + * @return self A new instance with the specified host + * @throws InvalidArgumentException for invalid hostnames + */ + public function withHost(string $host, int $flags = 0): self + { + $clone = $this->clone($flags); + $clone->setHost($host, $flags); + return $clone; + } + + /** + * Return an instance with the specified port. + * @param null|int $port The port to use with the new instance + * @param int $flags Optional modifier flags + * @return self A new instance with the specified port + * @throws InvalidArgumentException for invalid ports + */ + public function withPort(int|null $port, int $flags = 0): self + { + $clone = $this->clone($flags); + $clone->setPort($port, $flags); + return $clone; + } + + /** + * Return an instance with the specified path. + * @param string $path The path to use with the new instance + * @param int $flags Optional modifier flags + * @return self A new instance with the specified path + * @throws InvalidArgumentException for invalid paths + */ + public function withPath(string $path, int $flags = 0): self + { + $clone = $this->clone($flags); + $clone->setPath($path, $flags); + return $clone; + } + + /** + * Return an instance with the specified query string. + * @param string $query The query string to use with the new instance + * @param int $flags Optional modifier flags + * @return self A new instance with the specified query string + * @throws InvalidArgumentException for invalid query strings + */ + public function withQuery(string $query, int $flags = 0): self + { + $clone = $this->clone($flags); + $clone->setQuery($query, $flags); + return $clone; + } + + /** + * Return an instance with the specified URI fragment. + * @param string $fragment The fragment to use with the new instance + * @param int $flags Optional modifier flags + * @return self A new instance with the specified fragment + */ + public function withFragment(string $fragment, int $flags = 0): self + { + $clone = $this->clone($flags); + $clone->setFragment($fragment, $flags); + return $clone; + } + + + // ---------- PSR-7 string & Stringable --------------------------------------------------------------------------- + + /** + * Return the string representation as a URI reference. + * @return string + */ + public function __toString(): string + { + return $this->toString(); + } + + + // ---------- JsonSerializable ------------------------------------------------------------------------------------ + + /** + * Return JSON encode value as URI reference. + * @return string + */ + public function jsonSerialize(): string + { + return $this->toString(); + } + + + // ---------- Equalable ------------------------------------------------------------------------------------------ + + /** + * Return JSON encode value as URI reference. + * @param UriInterface|string $compareWith + * @return bool + */ + public function equals(mixed $compareWith): bool + { + if (!$compareWith instanceof UriInterface && !is_string($compareWith)) { + throw new IncomparableException(sprintf("Can not compare with type '%s'", get_debug_type($compareWith))); + } + $flags = self::REQUIRE_PORT | self::NORMALIZE_PATH | self::IDN_ENCODE; + $them = $compareWith instanceof self ? $compareWith : new self((string)$compareWith); + return $this->toString($flags) == $them->toString($flags); + } + + + // ---------- Extensions ------------------------------------------------------------------------------------------ + + /** + * Return the string representation as a URI reference. + * @param int $flags Optional modifier flags + * @param string $format Optional format specification + * @return string + */ + public function toString(int $flags = 0, string $format = '{scheme}{authority}{path}{query}{fragment}'): string + { + $pathFlags = ($this->authority && $this->path ? self::ABSOLUTE_PATH : 0) | $flags; + return str_replace([ + '{scheme}', + '{authority}', + '{path}', + '{query}', + '{fragment}', + ], [ + $this->formatComponent($this->getScheme($flags), '', ':'), + $this->authority ? "//{$this->formatComponent($this->getAuthority($flags))}" : '', + $this->formatComponent($this->getPath($pathFlags)), + $this->formatComponent($this->getQuery(), '?'), + $this->formatComponent($this->getFragment(), '#'), + ], $format); + } + + /** + * Get compontsns as array; as parse_url() method + * @param int $flags Optional modifier flags + * @return array + */ + public function getComponents(int $flags = 0): array + { + return array_filter([ + 'scheme' => $this->getScheme($flags), + 'host' => $this->getHost($flags), + 'port' => $this->getPort($flags | self::REQUIRE_PORT), + 'user' => $this->user, + 'pass' => $this->pass, + 'path' => $this->getPath($flags), + 'query' => $this->getQuery($flags), + 'fragment' => $this->getFragment($flags), + ]); + } + + /** + * Return an instance with the specified compontents set. + * @param array $components + * @param int<0, 256> $flags + * @return self A new instance with the specified components + */ + public function withComponents(array $components, int $flags = 0): self + { + $clone = $this->clone($flags); + foreach ($components as $component => $value) { + switch ($component) { + case 'port': + $clone->setPort($value, $flags); + break; + case 'scheme': + $clone->setScheme($value, $flags); + break; + case 'host': + $clone->setHost($value, $flags); + break; + case 'path': + $clone->setPath($value, $flags); + break; + case 'query': + $clone->setQuery($value, $flags); + break; + case 'fragment': + $clone->setFragment($value, $flags); + break; + case 'userInfo': + $clone->setUserInfo(...$value); + break; + default: + throw new InvalidArgumentException("Invalid URI component: '{$component}'"); + } + } + return $clone; + } + + /** + * Return all query items (if any) as associative array. + * @param int $flags Optional modifier flags + * @return array Query items + */ + public function getQueryItems(int $flags = 0): array + { + parse_str($this->getQuery(), $result); + return $result; + } + + /** + * Return query item value for named query item, or null if not present. + * @param string $name Name of query item to retrieve + * @param int $flags Optional modifier flags + * @return string|null|array Query item value + */ + public function getQueryItem(string $name, int $flags = 0): array|string|null + { + parse_str($this->getQuery(), $result); + return $result[$name] ?? null; + } + + /** + * Add query items as associative array that will be merged qith current items. + * @param array $items Array of query items to add + * @param int $flags Optional modifier flags + * @return self A new instance with the added query items + */ + public function withQueryItems(array $items, int $flags = 0): self + { + $clone = $this->clone($flags); + $clone->setQuery(http_build_query( + $this->queryMerge($this->getQueryItems($flags), $items), + '', + null, + PHP_QUERY_RFC3986 + ), $flags); + return $clone; + } + + /** + * Add query item value for named query item + * @param string $name Name of query item to add + * @param string|null|array $value Value of query item to add + * @param int $flags Optional modifier flags + * @return self A new instance with the added query items + */ + public function withQueryItem(string $name, array|string|null $value, int $flags = 0): self + { + return $this->withQueryItems([$name => $value], $flags); + } + + + // ---------- Protected helper methods ---------------------------------------------------------------------------- + + protected function setPort(int|null $port, int $flags = 0): void + { + if ($port !== null && ($port < 0 || $port > 65535)) { + throw new InvalidArgumentException("Invalid port '{$port}'"); + } + $this->port = $port; + } + + protected function setScheme(string $scheme, int $flags = 0): void + { + $pattern = '/^[a-z][a-z0-9-+.]*$/i'; + if ($scheme !== '' && preg_match($pattern, $scheme) == 0) { + throw new InvalidArgumentException("Invalid scheme '{$scheme}': Should match {$pattern}"); + } + $this->scheme = mb_strtolower($scheme); + } + + protected function setHost(string $host, int $flags = 0): void + { + $this->authority = $this->authority || $host !== ''; + if ($flags & self::IDNA) { + trigger_error("Flag IDNA is deprecated; use IDN_ENCODE instead", E_USER_DEPRECATED); + $host = $this->idnEncode($host); + } + if ($flags & self::IDN_ENCODE) { + $host = $this->idnEncode($host); + } + if ($flags & self::IDN_DECODE) { + $host = $this->idnDecode($host); + } + $this->host = mb_strtolower($host); + } + + protected function setPath(string $path, int $flags = 0): void + { + if ($flags & self::NORMALIZE_PATH) { + $path = $this->normalizePath($path); + } + if ($flags & self::ABSOLUTE_PATH && substr($path, 0, 1) !== '/') { + $path = "/{$path}"; + } + $this->path = $this->uriDecode($path); + } + + protected function setQuery(string $query, int $flags = 0): void + { + $this->query = $this->uriDecode($query); + } + + protected function setFragment(string $fragment, int $flags = 0): void + { + $this->fragment = $this->uriDecode($fragment); + } + + protected function setUser(string $user, int $flags = 0): void + { + $this->user = $this->uriDecode($user); + } + + protected function setPassword(string|null $pass, int $flags = 0): void + { + $this->pass = $pass === null ? null : $this->uriDecode($pass); + } + + protected function setUserInfo(string $user = '', string|null $pass = null, int $flags = 0): void + { + $this->setUser($user); + $this->setPassword($pass); + } + + + // ---------- Private helper methods ------------------------------------------------------------------------------ + + private function parse(string $uriString = ''): void + { + if ($uriString === '') { + return; + } + preg_match(self::RE_MAIN, $uriString, $main); + $this->authority = !empty($main['authorityc']); + $this->setScheme($main['scheme'] ?? ''); + $this->setPath($main['path'] ?? ''); + $this->setQuery($main['query'] ?? ''); + $this->setFragment($main['fragment'] ?? ''); + if ($this->authority && !empty($main['authority'])) { + preg_match(self::RE_AUTH, $main['authority'], $auth); + if (empty($auth)) { + throw new InvalidArgumentException("Invalid 'authority'."); + } + if ($auth['host'] === '' && $auth['user'] !== '') { + throw new InvalidArgumentException("Invalid 'authority'."); + } + $this->setUser($auth['user'] ?? ''); + $this->setPassword($auth['pass'] ?? null); + $this->setHost($auth['host'] ?? ''); + $this->setPort(isset($auth['port']) ? (int)$auth['port'] : null); + } + } + + private function clone(int $flags = 0): self + { + $clone = clone $this; + if ($flags & self::REQUIRE_PORT) { + $clone->setPort($this->getPort(self::REQUIRE_PORT), $flags); + } + return $clone; + } + + private function uriEncode(string $source, int $flags = 0, string $keep = ''): string + { + if ($flags & self::URI_DECODE) { + return $source; + } + + $unreserved = 'a-zA-Z0-9_\-\.~'; + $subdelim = '!\$&\'\(\)\*\+,;='; + $char = '\pL'; + $pct = '%(?![A-Fa-f0-9]{2}))'; + + $re = "/(?:[^%{$unreserved}{$subdelim}{$keep}]+|{$pct}/u"; + + if ($flags & self::URI_ENCODE) { + $re = "/(?:[^%{$unreserved}{$subdelim}{$keep}{$char}]+|{$pct}/u"; + } + return preg_replace_callback($re, function ($matches) { + return rawurlencode($matches[0]); + }, $source) ?? $source; + } + + private function uriDecode(string $source): string + { + $re = "/(%[A-Fa-f0-9]{2})/u"; + return preg_replace_callback($re, function ($matches) { + return rawurldecode($matches[0]); + }, $source) ?? $source; + } + + private function formatComponent(string|int|null $value, string $before = '', string $after = ''): string + { + $string = strval($value); + return $string === '' ? '' : "{$before}{$string}{$after}"; + } + + 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 idnEncode(string $value): string + { + if ($value === '' || !function_exists('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) ?: $value; + } + + private function idnDecode(string $value): string + { + if ($value === '' || !function_exists('idn_to_utf8')) { + return $value; // Can't convert, but don't cause exception + } + return idn_to_utf8($value, IDNA_NONTRANSITIONAL_TO_UNICODE, INTL_IDNA_VARIANT_UTS46) ?: $value; + } + + /** + * @param array $a + * @param array $b + * @return array + */ + private function queryMerge(array $a, array $b): array + { + foreach ($b as $key => $value) { + if (is_int($key)) { + $a[] = $value; + } elseif (is_array($value)) { + $a[$key] = $this->queryMerge($a[$key] ?? [], $b[$key] ?? []); + } elseif (is_scalar($value)) { + $a[$key] = $this->uriDecode($b[$key]); + } else { + unset($a[$key]); + } + } + return $a; + } +} diff --git a/vendor/phrity/net-uri/src/UriFactory.php b/vendor/phrity/net-uri/src/UriFactory.php new file mode 100644 index 000000000..4b9b91dc9 --- /dev/null +++ b/vendor/phrity/net-uri/src/UriFactory.php @@ -0,0 +1,47 @@ + 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 InvalidArgumentException; +use Psr\Http\Message\{ + UriFactoryInterface, + UriInterface +}; + +/** + * Net\UriFactory class. + */ +class UriFactory implements UriFactoryInterface +{ + // ---------- PSR-7 methods --------------------------------------------------------------------------------------- + + /** + * 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); + } + + + // ---------- Extensions ------------------------------------------------------------------------------------------ + + /** + * Create a new URI from existing. + * @param UriInterface $uri A URI instance to create from. + * @throws InvalidArgumentException If the given URI cannot be parsed + */ + public function createUriFromInterface(UriInterface $uri): UriInterface + { + return new Uri($uri->__toString()); + } +} diff --git a/vendor/phrity/util-accessor/LICENSE b/vendor/phrity/util-accessor/LICENSE new file mode 100644 index 000000000..09eaada6c --- /dev/null +++ b/vendor/phrity/util-accessor/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Sören Jensen + +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. diff --git a/vendor/phrity/util-accessor/composer.json b/vendor/phrity/util-accessor/composer.json new file mode 100644 index 000000000..33d626598 --- /dev/null +++ b/vendor/phrity/util-accessor/composer.json @@ -0,0 +1,35 @@ +{ + "name": "phrity/util-accessor", + "type": "library", + "description": "Utility to handle access to a data set by using access paths. Similar to and partly compatible with xpath and json-pointer.", + "homepage": "https://phrity.sirn.se/util-accessor", + "keywords": ["accessor"], + "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\\Test\\": "tests/" + } + }, + "require": { + "php": "^8.1", + "phrity/util-transformer": "^1.0" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", + "robiningelbrecht/phpunit-coverage-tools": "^1.9", + "squizlabs/php_codesniffer": "^3.5 || ^4.0" + } +} diff --git a/vendor/phrity/util-accessor/src/Accessor.php b/vendor/phrity/util-accessor/src/Accessor.php new file mode 100644 index 000000000..30f7d66e1 --- /dev/null +++ b/vendor/phrity/util-accessor/src/Accessor.php @@ -0,0 +1,65 @@ +separator = $separator; + $this->accessorTransformer = $transformer; + } + + /** + * Get specified content from data set. + * @param mixed $data Data set to access + * @param string $path Path to access + * @param mixed $default Default value + * @param string|null $coerce Optional type coercion + * @return mixed Specified content of data set + */ + public function get(mixed $data, string $path, mixed $default = null, string|null $coerce = null): mixed + { + return $this->accessorGet($data, $this->accessorParsePath($path, $this->separator), $default, $coerce); + } + + /** + * Check specified content in data set. + * @param mixed $data Data set to access + * @param string $path Path to access + * @return bool If speciefied content is present + */ + public function has(mixed $data, string $path): bool + { + return $this->accessorHas($data, $this->accessorParsePath($path, $this->separator)); + } + + /** + * Set specified content on data set. + * @param mixed $data Data set to modify + * @param string $path Path to access + * @param mixed $value Value to set + * @return mixed Modified data set + */ + public function set(mixed $data, string $path, mixed $value): mixed + { + return $this->accessorSet($data, $this->accessorParsePath($path, $this->separator), $value); + } +} diff --git a/vendor/phrity/util-accessor/src/AccessorException.php b/vendor/phrity/util-accessor/src/AccessorException.php new file mode 100644 index 000000000..a43e1d57d --- /dev/null +++ b/vendor/phrity/util-accessor/src/AccessorException.php @@ -0,0 +1,12 @@ + $path Path to access + * @param mixed $default Default value + * @param string|null $coerce Optional type coercion + * @return mixed Specified content of data set + */ + private function accessorGet(mixed $data, array $path, mixed $default, string|null $coerce = null): mixed + { + if (empty($path)) { + if ($coerce && $this->accessorGetTransformer()->canTransform($data, $coerce)) { + return $this->accessorGetTransformer()->transform($data, $coerce); + } + return $data; // Bottom case + } + $current = array_shift($path); + if ($this->accessorGetTransformer()->canTransform($data)) { + $data = $this->accessorGetTransformer()->transform($data); + } + if (is_array($data) && array_key_exists($current, $data)) { + return $this->accessorGet($data[$current], $path, $default, $coerce); + } + if (is_object($data) && property_exists($data, $current)) { + return $this->accessorGet($data->$current, $path, $default, $coerce); + } + return $default; // No match + } + + /** + * Recursive worker function for has() operation. + * @param mixed $data Data set to access + * @param array $path Path to access + * @return bool If speciefied content is present + */ + private function accessorHas(mixed $data, array $path): bool + { + if (empty($path)) { + return true; // Bottom case + } + $current = array_shift($path); + if ($this->accessorGetTransformer()->canTransform($data)) { + $data = $this->accessorGetTransformer()->transform($data); + } + if (is_array($data) && array_key_exists($current, $data)) { + return $this->accessorHas($data[$current], $path); + } + if (is_object($data) && property_exists($data, $current)) { + return $this->accessorHas($data->$current, $path); + } + return false; // No match + } + + /** + * Recursive worker function for set() operation. + * @param mixed $data Data set to modify + * @param array $path Path to access + * @param mixed $value Value to set + * @return mixed Modified data set + */ + private function accessorSet(mixed $data, array $path, mixed $value): mixed + { + if (empty($path)) { + return $value; // Bottom case + } + $current = array_shift($path); + if ($this->accessorGetTransformer()->canTransform($data)) { + $data = $this->accessorGetTransformer()->transform($data); + } + if (is_object($data)) { + $data->$current = $this->accessorSet($data->$current ?? null, $path, $value); + } + if (is_array($data)) { + $data[$current] = $this->accessorSet($data[$current] ?? null, $path, $value); + } + if (is_null($data) || is_scalar($data)) { + $data = []; + $data[$current] = $this->accessorSet(null, $path, $value); + } + return $data; + } + + /** + * Parse string path into array segments. + * @param string $path Path to parse + * @param non-empty-string $separator Separator token + * @return array Path segments as array + */ + private function accessorParsePath(string $path, string $separator): array + { + return array_filter(explode($separator, $path), function (string $item): bool { + return $item !== ''; + }); + } + + /** + * Get or set Transformer to use, BasicTypeConverter used as default. + * @return TransformerInterface + */ + private function accessorGetTransformer(): TransformerInterface + { + if (!$this->accessorTransformer) { + $this->accessorTransformer = new BasicTypeConverter(); // Default type converter + } + return $this->accessorTransformer; + } +} diff --git a/vendor/phrity/util-accessor/src/DataAccessor.php b/vendor/phrity/util-accessor/src/DataAccessor.php new file mode 100644 index 000000000..5437debf2 --- /dev/null +++ b/vendor/phrity/util-accessor/src/DataAccessor.php @@ -0,0 +1,76 @@ +data = $data; + $this->separator = $separator; + $this->accessorTransformer = $transformer; + } + + /** + * Get specified content from data set. + * @param string $path Path to access + * @param mixed $default Default value + * @param string|null $coerce Optional type coercion + * @return mixed Specified content of data set + */ + public function get(string $path, mixed $default = null, string|null $coerce = null): mixed + { + return $this->accessorGet($this->data, $this->accessorParsePath($path, $this->separator), $default, $coerce); + } + + /** + * Check specified content in data set. + * @param string $path Path to access + * @return bool If speciefied content is present + */ + public function has(string $path): bool + { + return $this->accessorHas($this->data, $this->accessorParsePath($path, $this->separator)); + } + + /** + * Set specified content on data set. + * @param string $path Path to access + * @param mixed $value Value to set + * @return mixed Modified data set + */ + public function set(string $path, mixed $value): mixed + { + $this->data = $this->accessorSet($this->data, $this->accessorParsePath($path, $this->separator), $value); + return $this->data; + } + + public function jsonSerialize(): mixed + { + return $this->data; + } +} diff --git a/vendor/phrity/util-accessor/src/PathAccessor.php b/vendor/phrity/util-accessor/src/PathAccessor.php new file mode 100644 index 000000000..c0b178663 --- /dev/null +++ b/vendor/phrity/util-accessor/src/PathAccessor.php @@ -0,0 +1,68 @@ +path = $path; + $this->separator = $separator; + $this->accessorTransformer = $transformer; + } + + /** + * Get specified content from data set. + * @param mixed $data Data set to access + * @param mixed $default Default value + * @return mixed Specified content of data set + */ + public function get(mixed $data, mixed $default = null, string|null $coerce = null): mixed + { + return $this->accessorGet($data, $this->accessorParsePath($this->path, $this->separator), $default, $coerce); + } + + /** + * Check specified content in data set. + * @param mixed $data Data set to access + * @return bool If speciefied content is present + */ + public function has(mixed $data): bool + { + return $this->accessorHas($data, $this->accessorParsePath($this->path, $this->separator)); + } + + /** + * Set specified content on data set. + * @param mixed $data Data set to modify + * @param mixed $value Value to set + * @return mixed Modified data set + */ + public function set(mixed $data, mixed $value): mixed + { + return $this->accessorSet($data, $this->accessorParsePath($this->path, $this->separator), $value); + } +} diff --git a/vendor/phrity/util-errorhandler/LICENSE b/vendor/phrity/util-errorhandler/LICENSE new file mode 100644 index 000000000..09eaada6c --- /dev/null +++ b/vendor/phrity/util-errorhandler/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Sören Jensen + +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. diff --git a/vendor/phrity/util-errorhandler/composer.json b/vendor/phrity/util-errorhandler/composer.json new file mode 100644 index 000000000..4fc377d02 --- /dev/null +++ b/vendor/phrity/util-errorhandler/composer.json @@ -0,0 +1,34 @@ +{ + "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": "^8.1" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", + "robiningelbrecht/phpunit-coverage-tools": "^1.9", + "squizlabs/php_codesniffer": "^3.5 || ^4.0" + } +} diff --git a/vendor/phrity/util-errorhandler/src/ErrorHandler.php b/vendor/phrity/util-errorhandler/src/ErrorHandler.php new file mode 100644 index 000000000..70534dba1 --- /dev/null +++ b/vendor/phrity/util-errorhandler/src/ErrorHandler.php @@ -0,0 +1,131 @@ + Util > ErrorHandler + */ + +namespace Phrity\Util; + +use Closure; +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 Closure|Throwable|null $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 (callable(): mixed)|null Previously registered error handler, if any + */ + public function set(Closure|Throwable|null $handling = null, int $levels = E_ALL): callable|null + { + 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 Closure|Throwable|null $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, Closure|Throwable|null $handling = null, int $levels = E_ALL): mixed + { + $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 Closure|Throwable|null $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, Closure|Throwable|null $handling = null, int $levels = E_ALL): mixed + { + $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(Closure|Throwable|null $handling): Closure + { + 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 + * @param Closure|Throwable|null $handling + * @param ErrorException|non-empty-array $error + * @mixed $result + * @return mixed + * @throws Throwable + */ + private function handle(Closure|Throwable|null $handling, ErrorException|array $error, mixed $result = null): mixed + { + if (is_callable($handling)) { + return $handling($error, $result); + } + if (is_array($error)) { + $error = array_shift($error); + } + if ($handling instanceof Throwable) { + try { + /* @phpstan-ignore finally.exitPoint */ + throw $error; + } finally { + /* @phpstan-ignore finally.exitPoint */ + throw $handling; + } + } + throw $error; + } +} diff --git a/vendor/phrity/util-interpolator/LICENSE b/vendor/phrity/util-interpolator/LICENSE new file mode 100644 index 000000000..09eaada6c --- /dev/null +++ b/vendor/phrity/util-interpolator/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Sören Jensen + +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. diff --git a/vendor/phrity/util-interpolator/composer.json b/vendor/phrity/util-interpolator/composer.json new file mode 100644 index 000000000..cb37d5176 --- /dev/null +++ b/vendor/phrity/util-interpolator/composer.json @@ -0,0 +1,36 @@ +{ + "name": "phrity/util-interpolator", + "type": "library", + "description": "Interpolator class and trait.", + "homepage": "https://phrity.sirn.se/util-interpolator", + "keywords": ["interpolator", "interpolate"], + "license": "MIT", + "authors": [ + { + "name": "Sören Jensen", + "email": "sirn@sirn.se", + "homepage": "https://phrity.sirn.se" + } + ], + "autoload": { + "psr-4": { + "Phrity\\Util\\Interpolator\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Phrity\\Util\\Interpolator\\Test\\": "tests/" + } + }, + "require": { + "php": "^8.1", + "phrity/util-accessor": "^1.3", + "phrity/util-transformer": "^1.3" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", + "robiningelbrecht/phpunit-coverage-tools": "^1.9", + "squizlabs/php_codesniffer": "^3.5 || ^4.0" + } +} diff --git a/vendor/phrity/util-interpolator/src/Interpolator.php b/vendor/phrity/util-interpolator/src/Interpolator.php new file mode 100644 index 000000000..80472aa9c --- /dev/null +++ b/vendor/phrity/util-interpolator/src/Interpolator.php @@ -0,0 +1,34 @@ +|object $replacers + */ + public function interpolate( + string|Stringable $source, + array|object $replacers = [], + ): string { + return $this->traitInterpolate($source, $replacers, $this->separator, $this->transformer); + } +} diff --git a/vendor/phrity/util-interpolator/src/InterpolatorTrait.php b/vendor/phrity/util-interpolator/src/InterpolatorTrait.php new file mode 100644 index 000000000..b857b597b --- /dev/null +++ b/vendor/phrity/util-interpolator/src/InterpolatorTrait.php @@ -0,0 +1,47 @@ +|object $replacers + * @param non-empty-string $separator + * @param TransformerInterface|null $transformer + */ + public function interpolate( + string|Stringable $source, + array|object $replacers = [], + string $separator = '.', + TransformerInterface|null $transformer = null, + ): string { + $transformer = $transformer ?? new FirstMatchResolver([ + new DateTimeConverter(), + new EnumConverter(), + new ReadableConverter(), + new ThrowableConverter(), + new StringableConverter(), + new BasicTypeConverter(), + ]); + $accessor = new DataAccessor($replacers, $separator, $transformer); + $result = preg_replace_callback('/{([^{}]{1,})}/', function (array $matches) use ($accessor): string { + return $accessor->get($matches[1], $matches[0], Type::STRING); + }, $source); + return $result ?? $source; + } +} diff --git a/vendor/phrity/util-transformer/LICENSE b/vendor/phrity/util-transformer/LICENSE new file mode 100644 index 000000000..09eaada6c --- /dev/null +++ b/vendor/phrity/util-transformer/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Sören Jensen + +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. diff --git a/vendor/phrity/util-transformer/composer.json b/vendor/phrity/util-transformer/composer.json new file mode 100644 index 000000000..c29a8f89a --- /dev/null +++ b/vendor/phrity/util-transformer/composer.json @@ -0,0 +1,42 @@ +{ + "name": "phrity/util-transformer", + "type": "library", + "description": "Type transformers, normalizers and resolvers.", + "homepage": "https://phrity.sirn.se/util-transformer", + "keywords": ["type", "transformer", "normalizer", "resolver"], + "license": "MIT", + "authors": [ + { + "name": "Sören Jensen", + "email": "sirn@sirn.se", + "homepage": "https://phrity.sirn.se" + } + ], + "autoload": { + "psr-4": { + "Phrity\\Util\\Transformer\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Phrity\\Util\\Transformer\\Test\\": "tests/" + } + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", + "robiningelbrecht/phpunit-coverage-tools": "^1.9", + "squizlabs/php_codesniffer": "^3.5 || ^4.0", + "symfony/property-access": "^6.0 || ^7.0 || ^8.0", + "symfony/serializer": "^6.0 || ^7.0 || ^8.0", + "symfony/translation-contracts": "^3.0", + "symfony/uid": "^6.0 || ^7.0 || ^8.0" + }, + "suggests": { + "symfony/property-access": "Use Symfony normalizer as transformer", + "symfony/serializer": "Use Symfony normalizer as transformer" + } +} diff --git a/vendor/phrity/util-transformer/src/BasicTypeConverter.php b/vendor/phrity/util-transformer/src/BasicTypeConverter.php new file mode 100644 index 000000000..83ad22dc5 --- /dev/null +++ b/vendor/phrity/util-transformer/src/BasicTypeConverter.php @@ -0,0 +1,70 @@ + $supportedTypes */ + private static array $supportedTypes = [ + null, + Type::ARRAY, + Type::BOOLEAN, + Type::INTEGER, + Type::NULL, + Type::NUMBER, + Type::OBJECT, + Type::STRING, + ]; + + /** @var array $typeMap */ + private array $typeMap; + + /** @param array $typeMap */ + public function __construct(array $typeMap = []) + { + $this->typeMap = $typeMap; + } + + public function canTransform(mixed $subject, string|null $type = null): bool + { + return in_array($type, self::$supportedTypes); + } + + public function transform(mixed $subject, string|null $type = null): mixed + { + if (!in_array(gettype($subject), self::$supportedTypes)) { + $subject = get_debug_type($subject); // Can only extract type name + } + $subjectType = gettype($subject); + $targetType = $type ?? $this->typeMap[$subjectType] ?? $subjectType; + + if (!$this->canTransform($subject, $targetType)) { + throw new TransformerException("Converting '{$subjectType}' to '{$targetType}' is not supported."); + } + /** @var Type::ARRAY|Type::BOOLEAN|Type::INTEGER|Type::NULL|Type::NUMBER|Type::OBJECT|Type::STRING $targetType */ + return match ($targetType) { + Type::ARRAY => (array)match ($subjectType) { + Type::OBJECT => get_object_vars($subject), + default => $subject, + }, + Type::BOOLEAN => (bool)$subject, + Type::INTEGER => (int)match ($subjectType) { + Type::OBJECT => (bool)$subject, + default => $subject, + }, + Type::NULL => null, + Type::NUMBER => (float)match ($subjectType) { + Type::OBJECT => (bool)$subject, + default => $subject, + }, + Type::OBJECT => (object)match ($subjectType) { + Type::OBJECT => get_object_vars($subject), + default => $subject, + }, + Type::STRING => (string)match ($subjectType) { + Type::ARRAY, Type::OBJECT => get_debug_type($subject), + default => $subject, + }, + }; + } +} diff --git a/vendor/phrity/util-transformer/src/ChainedResolver.php b/vendor/phrity/util-transformer/src/ChainedResolver.php new file mode 100644 index 000000000..cb73f0e5f --- /dev/null +++ b/vendor/phrity/util-transformer/src/ChainedResolver.php @@ -0,0 +1,56 @@ + $transformers */ + private array $transformers; + private string|null $default; + + /** + * @param array $transformers + */ + public function __construct(array $transformers, string|null $default = null) + { + foreach ($transformers as $transformer) { + if (!$transformer instanceof TransformerInterface) { + throw new InvalidArgumentException(sprintf( + "'%s' is not implementing %s", + get_debug_type($transformer), + TransformerInterface::class + )); + } + } + $this->transformers = $transformers; + $this->default = $default; + } + + public function canTransform(mixed $subject, string|null $type = null): bool + { + $type ??= $this->default; + foreach ($this->transformers as $transformer) { + if ($transformer->canTransform($subject, $type)) { + return true; + } + } + return false; + } + + public function transform(mixed $subject, string|null $type = null): mixed + { + $type ??= $this->default; + foreach ($this->transformers as $transformer) { + if ($transformer->canTransform($subject, $type)) { + $subject = $transformer->transform($subject, $type); + } + } + $subjectType = gettype($subject); + if ($type !== null && $type !== $subjectType) { + throw new TransformerException("Could not find transformer for '{$subjectType}'."); + } + return $subject; + } +} diff --git a/vendor/phrity/util-transformer/src/DateTimeConverter.php b/vendor/phrity/util-transformer/src/DateTimeConverter.php new file mode 100644 index 000000000..b520a0867 --- /dev/null +++ b/vendor/phrity/util-transformer/src/DateTimeConverter.php @@ -0,0 +1,50 @@ +canTransform($subject, $type)) { + throw new TransformerException("Converting '{$subjectType}' is not supported."); + } + if ($subject instanceof DateInterval) { + return $subject->format($this->dateIntervalFormat); + } + if ($subject instanceof DatePeriod) { + return sprintf( + '%s (%s)', + $subject->getStartDate()->format($this->dateTimeFormat), + $subject->getDateInterval()->format($this->dateIntervalFormat) + ); + } + if ($subject instanceof DateTimeInterface) { + return $subject->format($this->dateTimeFormat); + } + return $subject->getName(); + } +} diff --git a/vendor/phrity/util-transformer/src/EnumConverter.php b/vendor/phrity/util-transformer/src/EnumConverter.php new file mode 100644 index 000000000..3716823b5 --- /dev/null +++ b/vendor/phrity/util-transformer/src/EnumConverter.php @@ -0,0 +1,30 @@ +default = $perDefault ? Type::STRING : null; + } + + public function canTransform(mixed $subject, string|null $type = null): bool + { + $type ??= $this->default; + return $subject instanceof UnitEnum && $type == Type::STRING; + } + + public function transform(mixed $subject, string|null $type = null): mixed + { + $subjectType = get_debug_type($subject); + if (!$this->canTransform($subject, $type)) { + throw new TransformerException("Enum string for '{$subjectType}' is not supported."); + } + return $subject->name; + } +} diff --git a/vendor/phrity/util-transformer/src/FirstMatchResolver.php b/vendor/phrity/util-transformer/src/FirstMatchResolver.php new file mode 100644 index 000000000..7abdd5244 --- /dev/null +++ b/vendor/phrity/util-transformer/src/FirstMatchResolver.php @@ -0,0 +1,54 @@ + $transformers */ + private array $transformers; + private string|null $default; + + /** + * @param array $transformers + * @param string|null $default + */ + public function __construct(array $transformers, string|null $default = null) + { + foreach ($transformers as $transformer) { + if (!$transformer instanceof TransformerInterface) { + throw new InvalidArgumentException(sprintf( + "'%s' is not implementing %s", + get_debug_type($transformer), + TransformerInterface::class + )); + } + } + $this->transformers = $transformers; + $this->default = $default; + } + + public function canTransform(mixed $subject, string|null $type = null): bool + { + $type ??= $this->default; + foreach ($this->transformers as $transformer) { + if ($transformer->canTransform($subject, $type)) { + return true; + } + } + return false; + } + + public function transform(mixed $subject, string|null $type = null): mixed + { + $type ??= $this->default; + foreach ($this->transformers as $transformer) { + if ($transformer->canTransform($subject, $type)) { + return $transformer->transform($subject, $type); + } + } + $subjectType = get_debug_type($subject); + throw new TransformerException("Could not find transformer for '{$subjectType}'."); + } +} diff --git a/vendor/phrity/util-transformer/src/FlattenDecoder.php b/vendor/phrity/util-transformer/src/FlattenDecoder.php new file mode 100644 index 000000000..6a2400e4a --- /dev/null +++ b/vendor/phrity/util-transformer/src/FlattenDecoder.php @@ -0,0 +1,47 @@ +separator)) { + return $subject; + } + $re = "|^([{$this->separator}]*[^{$this->separator}]+){$this->separator}(.+)|"; + $coll = []; + foreach ($subject as $key => $value) { + if ($key == $this->separator) { + $coll[$this->separator] = $value; + continue; + } + preg_match($re, $key, $res); + $keyf = $res[1] ?? $key; + $keyl = $res[2] ?? $this->separator; + $coll[$keyf][$keyl] = $value; + } + foreach ($coll as $key => $sub) { + if (!is_array($sub)) { + continue; + } + $coll[$key] = $this->transform($sub); + } + if (count($coll) === 1 && isset($coll[$this->separator])) { + return $coll[$this->separator]; + } + return $coll; + } +} diff --git a/vendor/phrity/util-transformer/src/JsonDecoder.php b/vendor/phrity/util-transformer/src/JsonDecoder.php new file mode 100644 index 000000000..13ff3d74c --- /dev/null +++ b/vendor/phrity/util-transformer/src/JsonDecoder.php @@ -0,0 +1,44 @@ +decode($subject))]); + } catch (JsonException $e) { + return false; + } + } + + /** + * @throws TransformerException + */ + public function transform(mixed $subject, string|null $type = null): mixed + { + if (!$this->canTransform($subject, $type)) { + throw new TransformerException("JSON decoding for '{$subject}' is not supported."); + } + return $this->decode($subject); + } + + /** + * @throws JsonException + */ + private function decode(string $subject): mixed + { + return json_decode($subject, associative: $this->associative, flags: JSON_THROW_ON_ERROR); + } +} diff --git a/vendor/phrity/util-transformer/src/JsonSerializableConverter.php b/vendor/phrity/util-transformer/src/JsonSerializableConverter.php new file mode 100644 index 000000000..0c267486b --- /dev/null +++ b/vendor/phrity/util-transformer/src/JsonSerializableConverter.php @@ -0,0 +1,25 @@ +jsonSerialize())]); + } + + public function transform(mixed $subject, string|null $type = null): mixed + { + $subjectType = get_debug_type($subject); + if (!$this->canTransform($subject, $type)) { + throw new TransformerException("JSON serialize for '{$subjectType}' is not supported."); + } + return $subject->jsonSerialize(); + } +} diff --git a/vendor/phrity/util-transformer/src/ReadableConverter.php b/vendor/phrity/util-transformer/src/ReadableConverter.php new file mode 100644 index 000000000..f7427caae --- /dev/null +++ b/vendor/phrity/util-transformer/src/ReadableConverter.php @@ -0,0 +1,32 @@ +default = $perDefault ? Type::STRING : null; + } + + public function canTransform(mixed $subject, string|null $type = null): bool + { + $type ??= $this->default; + return in_array(gettype($subject), [Type::BOOLEAN, Type::NULL]) && $type == Type::STRING; + } + + public function transform(mixed $subject, string|null $type = null): mixed + { + $subjectType = gettype($subject); + if (!$this->canTransform($subject, $type)) { + throw new TransformerException("Creating readable for '{$subjectType}' is not supported."); + } + /** @var Type::BOOLEAN|Type::NULL $subjectType */ + return match ($subjectType) { + Type::BOOLEAN => $subject ? 'true' : 'false', + Type::NULL => 'null', + }; + } +} diff --git a/vendor/phrity/util-transformer/src/RecursionResolver.php b/vendor/phrity/util-transformer/src/RecursionResolver.php new file mode 100644 index 000000000..c8d865a4c --- /dev/null +++ b/vendor/phrity/util-transformer/src/RecursionResolver.php @@ -0,0 +1,38 @@ +transformer = $transformer; + } + + public function canTransform(mixed $subject, string|null $type = null): bool + { + return $this->transformer->canTransform($subject, $type); + } + + public function transform(mixed $subject, string|null $type = null): mixed + { + $transformed = $this->transformer->transform($subject, $type); + if (is_array($transformed)) { + foreach ($transformed as $key => $content) { + if (is_array($content) || is_object($content)) { + $transformed[$key] = $this->transform($content, $type); + } + } + } + if (is_object($transformed)) { + foreach ((array)$transformed as $key => $content) { + if (is_array($content) || is_object($content)) { + $transformed->$key = $this->transform($content, $type); + } + } + } + return $transformed; + } +} diff --git a/vendor/phrity/util-transformer/src/ReversedReadableConverter.php b/vendor/phrity/util-transformer/src/ReversedReadableConverter.php new file mode 100644 index 000000000..a024c43c4 --- /dev/null +++ b/vendor/phrity/util-transformer/src/ReversedReadableConverter.php @@ -0,0 +1,33 @@ +canTransform($subject, $type)) { + throw new TransformerException("Creating reversed readable for '{$subjectType}' is not supported."); + } + + /** @var "true"|"false"|"null"|"1"|"0"|"" $subject */ + $subject = strtolower($subject); + return match ($subject) { + 'true', '1' => true, + 'false', '0' => false, + 'null' => null, + '' => $type == Type::NULL ? null : false, + }; + } +} diff --git a/vendor/phrity/util-transformer/src/StringResolver.php b/vendor/phrity/util-transformer/src/StringResolver.php new file mode 100644 index 000000000..3ec30a187 --- /dev/null +++ b/vendor/phrity/util-transformer/src/StringResolver.php @@ -0,0 +1,77 @@ +converter = new BasicTypeConverter(); + $this->transformer = $transformer ?? $this->converter; + } + + public function canTransform(mixed $subject, string|null $type = null): bool + { + return true; + } + + public function transform(mixed $subject, string|null $type = null): mixed + { + $subjectType = get_debug_type($subject); + if ($this->transformer->canTransform($subject, $type)) { + // Convert using configured transformes and type + $subject = $this->transformer->transform($subject, $type); + } + if (is_string($subject)) { + // If we get string, done here + return $subjectType == Type::STRING ? $this->string($subject) : $subject; + } + if (is_object($subject)) { + // Force object result to array using basic type converter + $subject = $this->converter->transform($subject, Type::ARRAY); + } + if (is_array($subject)) { + // Descend into recursive worker + return $this->wrap($subject, $type, array_is_list($subject)); + } + // Force non-string scalars to string + return $this->converter->transform($subject, Type::STRING); + } + + /** + * @param array $items + */ + private function wrap(array $items, string|null $type, bool $isList): string + { + return sprintf( + '%s%s%s', + $isList ? '[' : '{', + implode(', ', $this->map($items, $type, $isList)), + $isList ? ']' : '}' + ); + } + + /** + * @param array $items + * @return array + */ + private function map(array $items, string|null $type, bool $isList): array + { + return array_map(function (mixed $value, mixed $key) use ($type, $isList): mixed { + return $this->index($this->transform($value, $type), $isList ? null : $key); + }, $items, array_keys($items)); + } + + private function index(mixed $value, string|int|null $key = null): string + { + return is_null($key) ? $value : "{$key}: {$value}"; + } + + private function string(mixed $value): string + { + return is_string($value) ? "\"{$value}\"" : $value; + } +} diff --git a/vendor/phrity/util-transformer/src/StringableConverter.php b/vendor/phrity/util-transformer/src/StringableConverter.php new file mode 100644 index 000000000..a0e8e36d0 --- /dev/null +++ b/vendor/phrity/util-transformer/src/StringableConverter.php @@ -0,0 +1,30 @@ +default = $perDefault ? Type::STRING : null; + } + + public function canTransform(mixed $subject, string|null $type = null): bool + { + $type ??= $this->default; + return $subject instanceof Stringable && $type == Type::STRING; + } + + public function transform(mixed $subject, string|null $type = null): mixed + { + $subjectType = get_debug_type($subject); + if (!$this->canTransform($subject, $type)) { + throw new TransformerException("Creating stringable for '{$subjectType}' is not supported."); + } + return $subject->__toString(); + } +} diff --git a/vendor/phrity/util-transformer/src/Symfony/NormalizerWrapper.php b/vendor/phrity/util-transformer/src/Symfony/NormalizerWrapper.php new file mode 100644 index 000000000..84b08e4e8 --- /dev/null +++ b/vendor/phrity/util-transformer/src/Symfony/NormalizerWrapper.php @@ -0,0 +1,36 @@ +normalizer->supportsNormalization($subject)) { + return false; + } + return in_array($type, [null, gettype($this->normalizer->normalize($subject))]); + } + + public function transform(mixed $subject, string|null $type = null): mixed + { + if (!$this->canTransform($subject, $type)) { + $subjectType = get_debug_type($subject); + $class = get_debug_type($this->normalizer); + throw new TransformerException("Normalizer {$class} to not support '{$subjectType}'."); + } + return $this->normalizer->normalize($subject); + } +} diff --git a/vendor/phrity/util-transformer/src/ThrowableConverter.php b/vendor/phrity/util-transformer/src/ThrowableConverter.php new file mode 100644 index 000000000..f87e419c8 --- /dev/null +++ b/vendor/phrity/util-transformer/src/ThrowableConverter.php @@ -0,0 +1,77 @@ + $supportedTypes */ + private static array $supportedTypes = [ + Type::ARRAY, + Type::OBJECT, + Type::STRING, + ]; + + private string $default; + /** @var array $parts */ + private array $parts; + + /** + * @param Type::ARRAY|Type::OBJECT|Type::STRING $default + * @param array $parts + */ + public function __construct(string $default = Type::OBJECT, array $parts = ['type', 'message', 'code']) + { + if (!in_array($default, self::$supportedTypes)) { + throw new InvalidArgumentException("Invalid '{$default}' provided"); + } + $this->default = $default; + $this->parts = $parts; + } + + public function canTransform(mixed $subject, string|null $type = null): bool + { + $type ??= $this->default; + return $subject instanceof Throwable && in_array($type, self::$supportedTypes); + } + + public function transform(mixed $subject, string|null $type = null): mixed + { + $subjectType = get_debug_type($subject); + $targetType = $type ?? $this->default; + + if (!$this->canTransform($subject, $targetType)) { + throw new TransformerException("Throwable conversion for '{$subjectType}' is not supported."); + } + /** @var Type::ARRAY|Type::OBJECT|Type::STRING $targetType */ + return match ($targetType) { + Type::ARRAY => $this->createData($subject), + Type::OBJECT => (object)$this->createData($subject), + Type::STRING => $subject->getMessage(), + }; + } + + /** + * @param Throwable $subject + * @return array + */ + private function createData(Throwable $subject): array + { + $data = []; + foreach ($this->parts as $part) { + $data[$part] = match ($part) { + 'type' => $subject::class, + 'message' => $subject->getMessage(), + 'code' => $subject->getCode(), + 'file' => $subject->getFile(), + 'line' => $subject->getLine(), + 'trace' => $subject->getTrace(), + 'previous' => $subject->getPrevious(), + default => null, + }; + } + return $data; + } +} diff --git a/vendor/phrity/util-transformer/src/TransformerException.php b/vendor/phrity/util-transformer/src/TransformerException.php new file mode 100644 index 000000000..d7f83d64b --- /dev/null +++ b/vendor/phrity/util-transformer/src/TransformerException.php @@ -0,0 +1,12 @@ + */ + use ListenerTrait; + use SendMethodsTrait; + use StringableTrait; + + private const SCOPE = 'client'; + + // Settings + /** @var array $headers */ + private array $headers = []; + + // Internal resources + private StreamFactory $streamFactory; + private Uri $socketUri; + private Connection|null $connection = null; + /** @var array $middlewares */ + private array $middlewares = []; + private Runner $runner; + private bool $running = false; + private HttpFactory $httpFactory; + /** @var non-empty-string $identity */ + private string $identity = 'client/'; + + + /* ---------- Magic methods ------------------------------------------------------------------------------------ */ + + /** + * @param UriInterface|string $uri A ws/wss-URI + * @param Configuration|null $configuration + * @param StreamFactory|null $streamFactory + */ + public function __construct( + UriInterface|string $uri, + Configuration|null $configuration = null, + StreamFactory|null $streamFactory = null, + ) { + $this->socketUri = $this->parseUri($uri); + $this->streamFactory = $streamFactory ?? new StreamFactory(); + $this->httpFactory = new DefaultHttpFactory(); + $this->identity = "client/{$this->socketUri->getHost()}"; + $this->initConfiguration($configuration); + $this->runner = new Runner($this->streamFactory); + } + + /** + * Get string representation of instance. + * @return string String representation + */ + public function __toString(): string + { + return $this->stringable('%s', $this->connection ? $this->socketUri->__toString() : 'closed'); + } + + + /* ---------- Configuration ------------------------------------------------------------------------------------ */ + + public function getIdentity(): string + { + return $this->identity; + } + + /** + * Set stream factory to use. + * @param StreamFactory $streamFactory + * @return self + * @depracated Remove in v4 + */ + public function setStreamFactory(StreamFactory $streamFactory): self + { + trigger_error('Client.setStreamFactory is deprecated and will be removed in v4.', E_USER_DEPRECATED); + $this->streamFactory = $streamFactory; + return $this; + } + + /** + * Set HTTP factory to use. + * @param HttpFactory $httpFactory + * @return self + */ + public function setHttpFactory(HttpFactory $httpFactory): self + { + $this->httpFactory = $httpFactory; + return $this; + } + + /** + * Set logger. + * @param LoggerInterface $logger Logger implementation + * @deprecated Will be removed in future version, set on Configuration instead + */ + public function setLogger(LoggerInterface $logger): void + { + $this->configuration->setLogger($logger); + if ($this->connection) { + $this->connection->setLogger($logger); + } + } + + /** + * Set timeout. + * @param int<0, max>|float $timeout Timeout in seconds + * @return self + * @throws InvalidArgumentException If invalid timeout provided + * @deprecated Will be removed in future version, set on Configuration instead + */ + public function setTimeout(int|float $timeout): self + { + $this->configuration->setTimeout($timeout); + if ($this->connection) { + $this->connection->setTimeout($timeout); + } + return $this; + } + + /** + * Get timeout. + * @return int<0, max>|float Timeout in seconds + * @deprecated Will be removed in future version, get from Configuration instead + */ + public function getTimeout(): int|float + { + return $this->configuration->getTimeout(); + } + + /** + * Set frame size. + * @param int<1, max> $frameSize Max frame payload size in bytes + * @return self + * @throws InvalidArgumentException If invalid frameSize provided + * @deprecated Will be removed in future version, set on Configuration instead + */ + public function setFrameSize(int $frameSize): self + { + $this->configuration->setFrameSize($frameSize); + if ($this->connection) { + $this->connection->setFrameSize($frameSize); + } + return $this; + } + + /** + * Get frame size. + * @return int Frame size in bytes + * @deprecated Will be removed in future version, get from Configuration instead + */ + public function getFrameSize(): int + { + return $this->configuration->getFrameSize(); + } + + /** + * Set connection persistence. + * @param bool $persistent True for persistent connection. + * @deprecated Will be removed in future version, set on Configuration instead + * @return self + */ + public function setPersistent(bool $persistent): self + { + $this->configuration->setPersistent($persistent); + return $this; + } + + /** + * Set stream context. + * @param Context|array $context Context or options as array + * @see https://www.php.net/manual/en/context.php + * @return self + * @deprecated Will be removed in future version, set on Configuration instead + */ + public function setContext(Context|array $context): self + { + if ($context instanceof Context) { + $this->configuration->setContext($context); + } else { + $this->configuration->getContext()->setOptions($context); + trigger_error('Calling Client.setContext with array is deprecated, use Context class.', E_USER_DEPRECATED); + } + return $this; + } + + /** + * Get current stream context. + * @return Context + * @deprecated Will be removed in future version, get from Configuration instead + */ + public function getContext(): Context + { + return $this->configuration->getContext(); + } + + /** + * Add header for handshake. + * @param string $name Header name + * @param string $content Header content + * @return self + */ + public function addHeader(string $name, string $content): self + { + $this->headers[$name] = $content; + return $this; + } + + /** + * Add a middleware. + * @param MiddlewareInterface $middleware + * @return self + */ + public function addMiddleware(MiddlewareInterface $middleware): self + { + $this->middlewares[] = $middleware; + if ($this->connection) { + $this->connection->addMiddleware($middleware); + } + return $this; + } + + + /* ---------- Messaging operations ----------------------------------------------------------------------------- */ + + /** + * Send message. + * @template T of Message + * @param T $message + * @return T + */ + public function send(Message $message): Message + { + return $this->connection()->pushMessage($message); + } + + /** + * Receive message. + * Note that this operation will block reading. + * @return Message + */ + public function receive(): Message + { + return $this->connection()->pullMessage(); + } + + + /* ---------- Listener operations ------------------------------------------------------------------------------ */ + + /** + * Start client listener. + * @throws ExceptionInterface On high level error + * @throws Throwable On low level error + */ + public function start(int|float|null $timeout = null): void + { + // Check if running + if ($this->running) { + $this->configuration->getLogger()->warning("[{scope}] Client is already running", [ + 'scope' => self::SCOPE, + 'client' => $this->identity, + ]); + return; + } + $this->running = true; + $reconnect = false; + $this->configuration->getLogger()->info("[{scope}] Client is running", [ + 'scope' => self::SCOPE, + 'client' => $this->identity, + ]); + + $connection = $this->connection(); + + // Run handler + while ($this->running) { + try { + // Run attached handlers on selected streams + $this->runner->handle($timeout ?? $this->configuration->getTimeout()); + + if (!$connection->isConnected()) { + $this->running = false; + } + $connection->tick(); + $this->dispatch('tick', [$this]); + } catch (CloseException $e) { + // Close connection + $connection->close($e->getCloseStatus(), $e->getMessage()); + $this->configuration->getLogger()->error("[{scope}] {message}", [ + 'scope' => self::SCOPE, + 'client' => $this->identity, + 'connection' => $connection->getIdentity(), + 'exception' => $e, + 'message' => $e->getMessage(), + ]); + $this->dispatch('error', [$this, $connection, $e]); + } catch (ReconnectException $e) { + // Reconnect connection + $reconnect = true; + if ($uri = $e->getUri()) { + $this->socketUri = $uri; + } + $connection->close(); + $this->configuration->getLogger()->error("[{scope}] {message}", [ + 'scope' => self::SCOPE, + 'client' => $this->identity, + 'connection' => $connection->getIdentity(), + 'exception' => $e, + 'message' => $e->getMessage(), + ]); + $this->dispatch('error', [$this, $connection, $e]); + } catch (ExceptionInterface $e) { + $this->disconnect(); + $this->running = false; + + // Low-level error + $this->configuration->getLogger()->error("[{scope}] {message}", [ + 'scope' => self::SCOPE, + 'client' => $this->identity, + 'connection' => $connection->getIdentity(), + 'exception' => $e, + 'message' => $e->getMessage(), + ]); + $this->dispatch('error', [$this, null, $e]); + } catch (Throwable $e) { + $this->disconnect(); + $this->running = false; + + // Crash it + $this->configuration->getLogger()->error("[{scope}] {message}", [ + 'scope' => self::SCOPE, + 'client' => $this->identity, + 'connection' => $connection->getIdentity(), + 'exception' => $e, + 'message' => $e->getMessage(), + ]); + throw $e; + } + gc_collect_cycles(); // Collect garbage + + if ($reconnect && !$connection->isConnected()) { + $reconnect = false; + $this->running = true; + } + } + } + + /** + * Stop client listener (resumable). + */ + public function stop(): void + { + $this->running = false; + $this->configuration->getLogger()->info("[{scope}] Client is stopped", [ + 'scope' => self::SCOPE, + 'client' => $this->identity, + ]); + } + + /** + * If client is running (accepting messages). + * @return bool + */ + public function isRunning(): bool + { + return $this->running; + } + + + /* ---------- Connection management ---------------------------------------------------------------------------- */ + + /** + * If Client has active connection. + * @return bool True if active connection. + */ + public function isConnected(): bool + { + return $this->connection && $this->connection->isConnected(); + } + + /** + * If Client is readable. + * @return bool + */ + public function isReadable(): bool + { + return $this->connection && $this->connection->isReadable(); + } + + /** + * If Client is writable. + * @return bool + */ + public function isWritable(): bool + { + return $this->connection && $this->connection->isWritable(); + } + + + /** + * Connect to server and perform upgrade. + * @throws ClientException On failed connection + */ + public function connect(): void + { + $this->disconnect(); + + $hostUri = (new Uri()) + ->withScheme(match ($this->socketUri->getScheme()) { + 'ws', 'http' => 'tcp', + 'wss', 'https' => 'ssl', + default => throw new ClientException("Invalid socket scheme: {$this->socketUri->getScheme()}") + }) + ->withHost($this->socketUri->getHost(Uri::IDN_ENCODE)) + ->withPort($this->socketUri->getPort(Uri::REQUIRE_PORT)); + + $stream = null; + + try { + $client = $this->streamFactory->createSocketClient($hostUri, $this->configuration->getContext()); + $client->setPersistent($this->configuration->isPersistent()); + $client->setTimeout($this->configuration->getTimeout()); + $stream = $client->connect(); + } catch (Throwable $e) { + $error = "Could not open socket to \"{$hostUri}\": {$e->getMessage()}"; + $this->configuration->getLogger()->error("[{scope}] {message}", [ + 'scope' => self::SCOPE, + 'client' => $this->identity, + 'exception' => $e, + 'message' => $e->getMessage(), + ]); + throw new ClientException($error); + } + $this->connection = $connection = new Connection( + $stream, + true, + false, + $hostUri->getScheme() === 'ssl', + $this->httpFactory, + clone $this->configuration + ); + + $this->runner->attach($this->connection, function (Runner $runner, Connection $connection) { + try { + // Read from connection + $message = $connection->pullMessage(); + $this->dispatch($message->getOpcode(), [$this, $connection, $message]); + } catch (MessageLevelInterface $e) { + // Error, but keep connection open + $this->configuration->getLogger()->error("[{scope}] {message}", [ + 'scope' => self::SCOPE, + 'client' => $this->identity, + 'connection' => $connection->getIdentity(), + 'exception' => $e, + 'message' => $e->getMessage(), + ]); + $this->dispatch('error', [$this, $connection, $e]); + } catch (ConnectionLevelInterface $e) { + // Error, disconnect connection + $this->disconnect(); + $this->configuration->getLogger()->error("[{scope}] {message}", [ + 'scope' => self::SCOPE, + 'client' => $this->identity, + 'connection' => $connection->getIdentity(), + 'exception' => $e, + 'message' => $e->getMessage(), + ]); + $this->dispatch('error', [$this, $connection, $e]); + } + }, $this->connection->getIdentity()); + + foreach ($this->middlewares as $middleware) { + $connection->addMiddleware($middleware); + } + + if (!$this->isConnected()) { + $this->configuration->getLogger()->error("[{scope}] Invalid stream on {uri}", [ + 'scope' => self::SCOPE, + 'client' => $this->identity, + 'connection' => $connection->getIdentity(), + 'uri' => $hostUri, + ]); + throw new ClientException("Invalid stream on \"{$hostUri}\"."); + } + try { + if (!$this->configuration->isPersistent() || $stream->tell() == 0) { + /** @throws ReconnectException */ + $response = $this->performHandshake($this->socketUri, $connection); + } + } catch (ReconnectException $e) { + $this->configuration->getLogger()->info("[{scope}] {message}", [ + 'scope' => self::SCOPE, + 'client' => $this->identity, + 'connection' => $connection->getIdentity(), + 'exception' => $e, + 'message' => $e->getMessage(), + ]); + if ($uri = $e->getUri()) { + $this->socketUri = $uri; + } + $this->connect(); + return; + } + $this->configuration->getLogger()->info("[{scope}] Client connected to {uri}", [ + 'scope' => self::SCOPE, + 'client' => $this->identity, + 'connection' => $connection->getIdentity(), + 'uri' => $this->socketUri, + ]); + $this->dispatch('handshake', [ + $this, + $connection, + $connection->getHandshakeRequest(), + $connection->getHandshakeResponse(), + ]); + $this->dispatch('connect', [$this, $connection, $connection->getHandshakeResponse()]); + } + + /** + * Disconnect from server. + */ + public function disconnect(): void + { + if ($this->connection) { + $this->runner->detach($this->connection->getIdentity()); + } + if ($this->connection && $this->isConnected()) { + $this->connection->disconnect(); + $this->configuration->getLogger()->info("[{scope}] Client disconnected", [ + 'scope' => self::SCOPE, + 'client' => $this->identity, + 'connection' => $this->connection->getIdentity(), + ]); + $this->dispatch('disconnect', [$this, $this->connection]); + } + } + + + /* ---------- Connection wrapper methods ----------------------------------------------------------------------- */ + + /** + * Get name of local socket, or null if not connected. + * @return string|null + */ + public function getName(): string|null + { + return $this->isConnected() ? $this->connection?->getName() : null; + } + + /** + * Get name of remote socket, or null if not connected. + * @return string|null + */ + public function getRemoteName(): string|null + { + return $this->isConnected() ? $this->connection?->getRemoteName() : null; + } + + /** + * Get meta value on connection. + * @param string $key Meta key + * @return mixed Meta value + * @deprecated Will be removed in v4 + */ + public function getMeta(string $key): mixed + { + trigger_error('Client.getMeta is deprecated and will be removed in v4.', E_USER_DEPRECATED); + return $this->isConnected() ? $this->connection?->getMeta($key) : null; + } + + /** + * Get Response for handshake procedure. + * @return ResponseInterface|null Handshake. + */ + public function getHandshakeResponse(): ResponseInterface|null + { + return $this->connection ? $this->connection->getHandshakeResponse() : null; + } + + + /* ---------- Internal helper methods -------------------------------------------------------------------------- */ + + /** + * Perform upgrade handshake on new connections. + * @throws HandshakeException On failed handshake + */ + protected function performHandshake(Uri $uri, Connection $connection): ResponseInterface + { + // Generate the WebSocket key. + $key = $this->generateKey(); + + $request = $this->httpFactory->createRequest('GET', $uri); + + $request = $request + ->withHeader('User-Agent', 'websocket-client-php') + ->withHeader('Connection', 'Upgrade') + ->withHeader('Upgrade', 'websocket') + ->withHeader('Sec-WebSocket-Key', $key) + ->withHeader('Sec-WebSocket-Version', '13'); + + // Handle basic authentication. + if ($userinfo = $uri->getUserInfo(Uri::URI_DECODE)) { + $request = $request->withHeader('Authorization', 'Basic ' . base64_encode($userinfo)); + } + + // Add and override with headers. + foreach ($this->headers as $name => $content) { + $request = $request->withHeader($name, $content); + } + + try { + /** @var RequestInterface */ + $request = $connection->pushHttp($request); + /** @var ResponseInterface */ + $response = $connection->pullHttp(); + if ($response->getStatusCode() != 101) { + throw new HandshakeException("Invalid status code {$response->getStatusCode()}.", $response); + } + + if (empty($response->getHeaderLine('Sec-WebSocket-Accept'))) { + throw new HandshakeException( + "Connection to '{$uri}' failed: Server sent invalid upgrade response.", + $response + ); + } + + $responseKey = trim($response->getHeaderLine('Sec-WebSocket-Accept')); + $expectedKey = base64_encode( + pack('H*', sha1($key . Constant::GUID)) + ); + + if ($responseKey !== $expectedKey) { + throw new HandshakeException("Server sent bad upgrade response.", $response); + } + } catch (HandshakeException $e) { + $this->configuration->getLogger()->error("[{scope}] {message}", [ + 'scope' => self::SCOPE, + 'client' => $this->identity, + 'connection' => $connection->getIdentity(), + 'exception' => $e, + 'message' => $e->getMessage(), + ]); + throw $e; + } + + $this->configuration->getLogger()->debug("[{scope}] Handshake on {path}", [ + 'scope' => self::SCOPE, + 'client' => $this->identity, + 'connection' => $connection->getIdentity(), + 'path' => $uri->getPath(), + ]); + + $connection->setHandshakeRequest($request); + $connection->setHandshakeResponse($response); + + return $response; + } + + /** + * Generate a random string for WebSocket key. + * @return string Random string + */ + protected function generateKey(): string + { + $key = ''; + for ($i = 0; $i < 16; $i++) { + $key .= chr(rand(33, 126)); + } + return base64_encode($key); + } + + /** + * Ensure URI instance to use in client. + * @param UriInterface|string $uri A ws/wss-URI + * @return Uri + * @throws BadUriException On invalid URI + */ + protected function parseUri(UriInterface|string $uri): Uri + { + try { + if ($uri instanceof Uri) { + $uriInstance = $uri; + } elseif ($uri instanceof UriInterface) { + $uriInstance = new Uri("{$uri}"); + } else { + $uriInstance = new Uri($uri); + } + } catch (InvalidArgumentException $e) { + throw new BadUriException("Invalid URI '{$uri}' provided."); + } + + + if (!in_array($uriInstance->getScheme(), ['ws', 'wss'])) { + throw new BadUriException("Invalid URI scheme, must be 'ws' or 'wss'."); + } + if (!$uriInstance->getHost()) { + throw new BadUriException("Invalid URI host."); + } + $uriInstance = $uriInstance->withPath($uriInstance->getPath(), Uri::ABSOLUTE_PATH | Uri::NORMALIZE_PATH); + return $uriInstance; + } + + protected function connection(): Connection + { + if (!$this->isConnected()) { + $this->connect(); + } + /** @var Connection */ + $connection = $this->connection; + return $connection; + } +} diff --git a/vendor/phrity/websocket/src/Configuration.php b/vendor/phrity/websocket/src/Configuration.php new file mode 100644 index 000000000..e90a988cb --- /dev/null +++ b/vendor/phrity/websocket/src/Configuration.php @@ -0,0 +1,195 @@ +|float $timeout */ + private int|float $timeout; + /** @var int<1, max> $frameSize */ + private int $frameSize; + private bool $persistent; + /** @var int<1, max>|null $maxConnections */ + private int|null $maxConnections; + + + /* ---------- Magic methods ------------------------------------------------------------------------------------ */ + + /** + * @param LoggerInterface|null $logger + * @param Context|null $context + * @param int<0, max>|float $timeout + * @param int<1, max> $frameSize + * @param bool $persistent + * @param int<1, max>|null $maxConnections + */ + public function __construct( + LoggerInterface|null $logger = null, + Context|null $context = null, + int|float|null $timeout = null, + int|null $frameSize = null, + bool|null $persistent = null, + int|null $maxConnections = null, + ) { + $this->setLogger($logger ?? new NullLogger()); + $this->setContext($context ?? new Context()); + $this->setTimeout($timeout ?? 60); + $this->setFrameSize($frameSize ?? 4096); + $this->setPersistent($persistent ?? false); + $this->setMaxConnections($maxConnections); + } + + public function __toString(): string + { + return $this->stringable(''); + } + + + /* ---------- Logger methods ----------------------------------------------------------------------------------- */ + + /** + * @return LoggerInterface $logger + */ + public function getLogger(): LoggerInterface + { + return $this->logger; + } + + /** + * @param LoggerInterface $logger + */ + public function setLogger(LoggerInterface $logger): void + { + $this->logger = $logger; + } + + + /* ---------- Context methods ---------------------------------------------------------------------------------- */ + + /** + * @return Context + */ + public function getContext(): Context + { + return $this->context; + } + + /** + * @param Context $context Context or options as array + */ + public function setContext(Context $context): void + { + $this->context = $context; + } + + + /* ---------- Timeout methods ---------------------------------------------------------------------------------- */ + + /** + * @return int<0, max>|float Timeout in seconds + */ + public function getTimeout(): int|float + { + return $this->timeout; + } + + /** + * @param int<0, max>|float $timeout Timeout in seconds + * @throws InvalidArgumentException If invalid timeout provided + */ + public function setTimeout(int|float $timeout): void + { + if ($timeout < 0) { + throw new InvalidArgumentException("Invalid timeout '{$timeout}' provided"); + } + $this->timeout = $timeout; + } + + + /* ---------- FrameSize methods -------------------------------------------------------------------------------- */ + + /** + * @return int<1, max> Frame size in bytes + */ + public function getFrameSize(): int + { + return $this->frameSize; + } + + /** + * @param int<1, max> $frameSize Max frame payload size in bytes + * @throws InvalidArgumentException If invalid frameSize provided + */ + public function setFrameSize(int $frameSize): void + { + if ($frameSize < 1) { + throw new InvalidArgumentException("Invalid frameSize '{$frameSize}' provided"); + } + $this->frameSize = $frameSize; + } + + + /* ---------- Persistent methods (Client only) ----------------------------------------------------------------- */ + + /** + * @return bool $persistent + */ + public function isPersistent(): bool + { + return $this->persistent; + } + + /** + * @param bool $persistent True for persistent connection + */ + public function setPersistent(bool $persistent): void + { + $this->persistent = $persistent; + } + + + /* ---------- Max connections (Server only) -------------------------------------------------------------------- */ + + /** + * @return int<1, max> Max connections allowed + */ + public function getMaxConnections(): int|null + { + return $this->maxConnections; + } + + /** + * @param int<1, max>|null $maxConnections + * @throws InvalidArgumentException If invalid number provided + */ + public function setMaxConnections(int|null $maxConnections): void + { + if ($maxConnections !== null && $maxConnections < 1) { + throw new InvalidArgumentException("Invalid maxConnections '{$maxConnections}' provided"); + } + $this->maxConnections = $maxConnections; + } +} diff --git a/vendor/phrity/websocket/src/Connection.php b/vendor/phrity/websocket/src/Connection.php new file mode 100644 index 000000000..41de22807 --- /dev/null +++ b/vendor/phrity/websocket/src/Connection.php @@ -0,0 +1,498 @@ + $meta */ + private array $meta = []; + /** @var non-empty-string $identity */ + private string $identity = '*/connection/'; + + + /* ---------- Magic methods ------------------------------------------------------------------------------------ */ + + public function __construct( + SocketStream $stream, + bool $pushMasked, + bool $pullMaskedRequired, + bool $ssl = false, + HttpFactory|null $httpFactory = null, + Configuration|null $configuration = null, + ) { + $this->stream = $stream; + $this->httpHandler = new HttpHandler($this->stream, $ssl, $httpFactory); + $this->messageHandler = new MessageHandler(new FrameHandler($this->stream, $pushMasked, $pullMaskedRequired)); + $this->middlewareHandler = new MiddlewareHandler($this->messageHandler, $this->httpHandler); + $this->localName = $this->stream->getLocalName() ?? ''; + $this->remoteName = $this->stream->getRemoteName() ?? ''; + $this->identity = sprintf( + '*/connection/%s/%s', + $this->getIdentityPart($this->localName), + $this->getIdentityPart($this->remoteName), + ); + $this->initConfiguration($configuration); + $this->stream->setTimeout($this->configuration->getTimeout()); + } + + public function __toString(): string + { + return $this->stringable('%s:%s', $this->localName, $this->remoteName); + } + + + /* ---------- Configuration ------------------------------------------------------------------------------------ */ + + public function getIdentity(): string + { + return $this->identity; + } + + /** + * Set logger. + * @param LoggerInterface $logger Logger implementation + * @deprecated Will be removed in future version, set on Configuration instead + */ + public function setLogger(LoggerInterface $logger): void + { + $this->configuration->setLogger($logger); + $this->messageHandler->setLogger($logger); + $this->middlewareHandler->setLogger($logger); + $this->configuration->getLogger()->debug("[{scope}] Setting logger: {logger}", [ + 'scope' => self::SCOPE, + 'connection' => $this->identity, + 'logger' => get_class($logger), + ]); + } + + /** + * Set time out on connection. + * @param int<0, max>|float $timeout Timeout part in seconds + * @return self + * @throws InvalidArgumentException + * @deprecated Will be removed in future version, set on Configuration instead + */ + public function setTimeout(int|float $timeout): self + { + $this->configuration->setTimeout($timeout); + $this->stream->setTimeout($timeout); + $this->configuration->getLogger()->debug("[{scope}] Setting timeout: {timeout} seconds", [ + 'scope' => self::SCOPE, + 'connection' => $this->identity, + 'timeout' => $timeout, + ]); + return $this; + } + + /** + * Get timeout. + * @return int<0, max>|float Timeout in seconds. + * @deprecated Will be removed in future version, get from Configuration instead + */ + public function getTimeout(): int|float + { + return $this->configuration->getTimeout(); + } + + /** + * Set frame size. + * @param int<1, max> $frameSize Frame size in bytes. + * @return self + * @throws InvalidArgumentException + * @deprecated Will be removed in future version, set on Configuration instead + */ + public function setFrameSize(int $frameSize): self + { + $this->configuration->setFrameSize($frameSize); + return $this; + } + + /** + * Get frame size. + * @return int<1, max> Frame size in bytes + * @deprecated Will be removed in future version, get from Configuration instead + */ + public function getFrameSize(): int + { + return $this->configuration->getFrameSize(); + } + + /** + * Get current stream context. + * @return Context + */ + public function getContext(): Context + { + return $this->stream->getContext(); + } + + /** + * Add a middleware. + * @param MiddlewareInterface $middleware + * @return self + */ + public function addMiddleware(MiddlewareInterface $middleware): self + { + $this->middlewareHandler->add($middleware); + $this->configuration->getLogger()->debug("[{scope}] Added middleware: {middleware}", [ + 'scope' => self::SCOPE, + 'connection' => $this->identity, + 'middleware' => $middleware, + ]); + return $this; + } + + + /* ---------- Connection management ---------------------------------------------------------------------------- */ + + /** + * If connected to stream. + * @return bool + */ + public function isConnected(): bool + { + return $this->stream->isConnected(); + } + + /** + * If connection is readable. + * @return bool + */ + public function isReadable(): bool + { + return $this->stream->isReadable(); + } + + /** + * If connection is writable. + * @return bool + */ + public function isWritable(): bool + { + return $this->stream->isWritable(); + } + + /** + * Close connection stream. + * @return self + */ + public function disconnect(): self + { + $this->configuration->getLogger()->info("[{scope}] Closing connection", [ + 'scope' => self::SCOPE, + 'connection' => $this->identity, + ]); + $this->stream->close(); + return $this; + } + + /** + * Close connection stream reading. + * @return self + */ + public function closeRead(): self + { + $this->configuration->getLogger()->info("[{scope}] Closing further reading", [ + 'scope' => self::SCOPE, + 'connection' => $this->identity, + ]); + $this->stream->closeRead(); + return $this; + } + + /** + * Close connection stream writing. + * @return self + */ + public function closeWrite(): self + { + $this->configuration->getLogger()->info("[{scope}] Closing further writing", [ + 'scope' => self::SCOPE, + 'connection' => $this->identity, + ]); + $this->stream->closeWrite(); + return $this; + } + + + /* ---------- Connection state --------------------------------------------------------------------------------- */ + + /** + * Get name of local socket, or null if not connected. + * @return string|null + */ + public function getName(): string|null + { + return $this->localName; + } + + /** + * Get name of remote socket, or null if not connected. + * @return string|null + */ + public function getRemoteName(): string|null + { + return $this->remoteName; + } + + /** + * Set meta value on connection. + * @param string $key Meta key + * @param mixed $value Meta value + */ + public function setMeta(string $key, mixed $value): void + { + $this->meta[$key] = $value; + } + + /** + * Get meta value on connection. + * @param string $key Meta key + * @return mixed Meta value + */ + public function getMeta(string $key): mixed + { + return $this->meta[$key] ?? null; + } + + /** + * Tick operation on connection. + */ + public function tick(): void + { + $this->middlewareHandler->processTick($this); + } + + + /* ---------- WebSocket Message methods ------------------------------------------------------------------------ */ + + /** + * Send message. + * @template T of Message + * @param T $message + * @return T + */ + public function send(Message $message): Message + { + return $this->pushMessage($message); + } + + /** + * Push a message to stream. + * @template T of Message + * @param T $message + * @return T + */ + public function pushMessage(Message $message): Message + { + try { + /** @throws Throwable */ + return $this->middlewareHandler->processOutgoing($this, $message); + } catch (Throwable $e) { + $this->throwException($e); + } + } + + /** + * Pull a message from stream + * @throws ExceptionInterface + */ + public function pullMessage(): Message + { + try { + /** @throws Throwable */ + return $this->middlewareHandler->processIncoming($this); + } catch (Throwable $e) { + $this->throwException($e); + } + } + + + /* ---------- HTTP Message methods ----------------------------------------------------------------------------- */ + + public function pushHttp(MessageInterface $message): MessageInterface + { + try { + /** @throws Throwable */ + return $this->middlewareHandler->processHttpOutgoing($this, $message); + } catch (Throwable $e) { + $this->throwException($e); + } + } + + public function pullHttp(): MessageInterface + { + try { + /** @throws Throwable */ + return $this->middlewareHandler->processHttpIncoming($this); + } catch (Throwable $e) { + $this->throwException($e); + } + } + + public function setHandshakeRequest(RequestInterface $request): self + { + $this->handshakeRequest = $request; + return $this; + } + + public function getHandshakeRequest(): RequestInterface|null + { + return $this->handshakeRequest; + } + + public function setHandshakeResponse(ResponseInterface $response): self + { + $this->handshakeResponse = $response; + return $this; + } + + public function getHandshakeResponse(): ResponseInterface|null + { + return $this->handshakeResponse; + } + + public function getStream(): SocketStream + { + return $this->stream; + } + + + /* ---------- Internal helper methods -------------------------------------------------------------------------- */ + + /** + * @throws ReconnectException + * @throws ExceptionInterface + * @throws ConnectionTimeoutException + * @throws ConnectionClosedException + * @throws ConnectionFailureException + */ + protected function throwException(Throwable $e): never + { + // Internal exceptions are handled and re-thrown + if ($e instanceof ReconnectException) { + $this->configuration->getLogger()->info("[{scope}] {message}", [ + 'scope' => self::SCOPE, + 'connection' => $this->identity, + 'exception' => $e, + 'message' => $e->getMessage(), + ]); + throw $e; + } + if ($e instanceof ExceptionInterface) { + $this->configuration->getLogger()->error("[{scope}] {message}", [ + 'scope' => self::SCOPE, + 'connection' => $this->identity, + 'exception' => $e, + 'message' => $e->getMessage(), + ]); + throw $e; + } + // External exceptions are converted to internal + if ($this->isConnected()) { + $meta = $this->stream->getMetadata(); + $json = json_encode($meta); + if (!empty($meta['timed_out'])) { + $this->configuration->getLogger()->error("[{scope}] {message}", [ + 'scope' => self::SCOPE, + 'connection' => $this->identity, + 'exception' => $e, + 'message' => $e->getMessage(), + 'meta' => $meta + ]); + throw new ConnectionTimeoutException(); + } + if (!empty($meta['eof'])) { + $this->configuration->getLogger()->error("[{scope}] {message}", [ + 'scope' => self::SCOPE, + 'connection' => $this->identity, + 'exception' => $e, + 'message' => $e->getMessage(), + 'meta' => $meta + ]); + throw new ConnectionClosedException(); + } + } + $this->configuration->getLogger()->error("[{scope}] {message}", [ + 'scope' => self::SCOPE, + 'connection' => $this->identity, + 'exception' => $e, + 'message' => $e->getMessage(), + ]); + throw new ConnectionFailureException(); + } + + protected function getIdentityPart(string $source): string + { + preg_match('/([0-9]+)$/', $source, $result); + return empty($result) ? $source : array_shift($result); + } +} diff --git a/vendor/phrity/websocket/src/Constant.php b/vendor/phrity/websocket/src/Constant.php new file mode 100644 index 000000000..a18c2459d --- /dev/null +++ b/vendor/phrity/websocket/src/Constant.php @@ -0,0 +1,17 @@ +status = $status; + parent::__construct($content); + } + + public function getCloseStatus(): int + { + return $this->status ?? 1000; + } +} diff --git a/vendor/phrity/websocket/src/Exception/ConnectionClosedException.php b/vendor/phrity/websocket/src/Exception/ConnectionClosedException.php new file mode 100644 index 000000000..e8f423ca9 --- /dev/null +++ b/vendor/phrity/websocket/src/Exception/ConnectionClosedException.php @@ -0,0 +1,20 @@ +response = $response; + } + + public function getResponse(): ResponseInterface + { + return $this->response; + } +} diff --git a/vendor/phrity/websocket/src/Exception/MessageLevelInterface.php b/vendor/phrity/websocket/src/Exception/MessageLevelInterface.php new file mode 100644 index 000000000..094d86261 --- /dev/null +++ b/vendor/phrity/websocket/src/Exception/MessageLevelInterface.php @@ -0,0 +1,16 @@ +uri = $uri; + parent::__construct("Reconnect requested" . ($uri ? ": {$uri}" : '')); + } + + public function getUri(): Uri|null + { + return $this->uri; + } +} diff --git a/vendor/phrity/websocket/src/Exception/RunnerException.php b/vendor/phrity/websocket/src/Exception/RunnerException.php new file mode 100644 index 000000000..ec6f54076 --- /dev/null +++ b/vendor/phrity/websocket/src/Exception/RunnerException.php @@ -0,0 +1,16 @@ +opcode = $opcode; + $this->payload = $payload; + $this->final = $final; + $this->rsv1 = $rsv1; + $this->rsv2 = $rsv2; + $this->rsv3 = $rsv3; + } + + public function isFinal(): bool + { + return $this->final; + } + + public function getRsv1(): bool + { + return $this->rsv1; + } + + public function setRsv1(bool $rsv1): void + { + $this->rsv1 = $rsv1; + } + + public function getRsv2(): bool + { + return $this->rsv2; + } + + public function getRsv3(): bool + { + return $this->rsv3; + } + + public function isContinuation(): bool + { + return $this->opcode === 'continuation'; + } + + public function getOpcode(): string + { + return $this->opcode; + } + + public function getPayload(): string + { + return $this->payload; + } + + public function getPayloadLength(): int + { + return strlen($this->payload); + } + + public function __toString(): string + { + return $this->stringable('%s', $this->opcode); + } +} diff --git a/vendor/phrity/websocket/src/Frame/FrameHandler.php b/vendor/phrity/websocket/src/Frame/FrameHandler.php new file mode 100644 index 000000000..11c52b098 --- /dev/null +++ b/vendor/phrity/websocket/src/Frame/FrameHandler.php @@ -0,0 +1,236 @@ +stream = $stream; + $this->pushMasked = $pushMasked; + $this->pullMaskedRequired = $pullMaskedRequired; + $this->initConfiguration($configuration); + } + + /** + * Set logger. + * @param LoggerInterface $logger Logger implementation + * @deprecated Will be removed in future version, set on Configuration instead + */ + public function setLogger(LoggerInterface $logger): void + { + $this->configuration->setLogger($logger); + } + + /** + * Pull frame from stream + * @throws CloseException + */ + public function pull(): Frame + { + // Read the frame "header" first, two bytes. + $data = $this->read(2); + list ($byte1, $byte2) = array_values($this->unpack('C*', $data)); + $final = (bool)($byte1 & 0b10000000); // Final fragment marker. + $rsv1 = (bool)($byte1 & 0b01000000); + $rsv2 = (bool)($byte1 & 0b00100000); + $rsv3 = (bool)($byte1 & 0b00010000); + + // Parse opcode + $opcodeInt = $byte1 & 0b00001111; + $opcodeInts = array_flip(self::$opcodes); + $opcode = array_key_exists($opcodeInt, $opcodeInts) ? $opcodeInts[$opcodeInt] : strval($opcodeInt); + + // Masking bit + $masked = (bool)($byte2 & 0b10000000); + + $payload = ''; + + // Payload length + $payloadLength = $byte2 & 0b01111111; + + if ($payloadLength > 125) { + if ($payloadLength === 126) { + $data = $this->read(2); // 126: Payload length is a 16-bit unsigned int + $payloadLength = current($this->unpack('n', $data)); + } else { + $data = $this->read(8); // 127: Payload length is a 64-bit unsigned int + $payloadLength = current($this->unpack('J', $data)); + } + } + + // Get masking key. + if ($masked) { + $maskingKey = $this->read(4); + } + + // Get the actual payload, if any (might not be for e.g. close frames). + if ($payloadLength > 0) { + $data = $this->read($payloadLength); + if ($masked) { + // Unmask payload. + for ($i = 0; $i < $payloadLength; $i++) { + $payload .= ($data[$i] ^ $maskingKey[$i % 4]); + } + } else { + $payload = $data; + } + } + + $frame = new Frame($opcode, $payload, $final, $rsv1, $rsv2, $rsv3); + $this->configuration->getLogger()->debug("[{scope}] Pulled '{opcode}' frame", [ + 'scope' => self::SCOPE, + 'opcode' => $frame->getOpcode(), + 'final' => $frame->isFinal(), + 'content-length' => $frame->getPayloadLength(), + ]); + if ($this->pullMaskedRequired && !$masked) { + $this->configuration->getLogger()->error("[{scope}] Masking required, but frame was unmasked", [ + 'scope' => self::SCOPE, + 'opcode' => $frame->getOpcode(), + ]); + throw new CloseException(1002, 'Masking required'); + } + + return $frame; + } + + // Push frame to stream + public function push(Frame $frame): int + { + $payload = $frame->getPayload(); + $payloadLength = $frame->getPayloadLength(); + + $data = ''; + $byte1 = $frame->isFinal() ? 0b10000000 : 0b00000000; // Final fragment marker. + $byte1 |= $frame->getRsv1() ? 0b01000000 : 0b00000000; // RSV1 bit. + $byte1 |= $frame->getRsv2() ? 0b00100000 : 0b00000000; // RSV2 bit. + $byte1 |= $frame->getRsv3() ? 0b00010000 : 0b00000000; // RSV3 bit. + $byte1 |= self::$opcodes[$frame->getOpcode()]; // Set opcode. + $data .= pack('C', $byte1); + + $byte2 = $this->pushMasked ? 0b10000000 : 0b00000000; // Masking bit marker. + + // 7 bits of payload length + if ($payloadLength > 65535) { + $data .= pack('C', $byte2 | 0b01111111); + $data .= pack('J', $payloadLength); + } elseif ($payloadLength > 125) { + $data .= pack('C', $byte2 | 0b01111110); + $data .= pack('n', $payloadLength); + } else { + $data .= pack('C', $byte2 | $payloadLength); + } + + // Handle masking. + if ($this->pushMasked) { + // Generate a random mask. + $mask = ''; + for ($i = 0; $i < 4; $i++) { + $mask .= chr(rand(0, 255)); + } + $data .= $mask; + + // Append masked payload to frame. + for ($i = 0; $i < $payloadLength; $i++) { + $data .= $payload[$i] ^ $mask[$i % 4]; + } + } else { + // Append payload as-is to frame. + $data .= $payload; + } + + // Write to stream. + $written = $this->write($data); + + $this->configuration->getLogger()->debug("[{scope}] Pushed '{opcode}' frame", [ + 'scope' => self::SCOPE, + 'opcode' => $frame->getOpcode(), + 'final' => $frame->isFinal(), + 'content-length' => $frame->getPayloadLength(), + ]); + return $written; + } + + /** + * Secured read op + * @param int<1, max> $length + * @throws RuntimeException + */ + private function read(int $length): string + { + $data = ''; + $read = 0; + while ($read < $length) { + /** @var int<1, max> $readLength */ + $readLength = $length - $read; + $got = $this->stream->read($readLength); + if (empty($got)) { + throw new RuntimeException('Empty read; connection dead?'); + } + $data .= $got; + $read = strlen($data); + } + return $data; + } + + /** + * Secured write op + * @throws RuntimeException + */ + private function write(string $data): int + { + $length = strlen($data); + $written = $this->stream->write($data); + if ($written < $length) { + throw new RuntimeException("Could only write {$written} out of {$length} bytes."); + } + return $written; + } + + /** @return array */ + private function unpack(string $format, string $string): array + { + /** @var array $result */ + $result = unpack($format, $string); + return $result; + } +} diff --git a/vendor/phrity/websocket/src/Http/DefaultHttpFactory.php b/vendor/phrity/websocket/src/Http/DefaultHttpFactory.php new file mode 100644 index 000000000..83595eacc --- /dev/null +++ b/vendor/phrity/websocket/src/Http/DefaultHttpFactory.php @@ -0,0 +1,72 @@ +uriFactory = new UriFactory(); + } + + /** + * Create a new request. + * @param string $method + * @param UriInterface|string $uri + */ + public function createRequest(string $method, mixed $uri): RequestInterface + { + return new Request($method, $uri); + } + + /** + * @param int $code + * @param string $reasonPhrase + */ + public function createResponse(int $code = 200, string $reasonPhrase = ''): ResponseInterface + { + return new Response($code, $reasonPhrase); + } + + /** + * @param string $method + * @param UriInterface|string $uri + * @param array $serverParams + */ + public function createServerRequest(string $method, mixed $uri, array $serverParams = []): ServerRequestInterface + { + return new ServerRequest($method, $uri); + } + + /** + * @param string $uri The URI to parse. + */ + public function createUri(string $uri = ''): UriInterface + { + return $this->uriFactory->createUri($uri); + } +} diff --git a/vendor/phrity/websocket/src/Http/HttpHandler.php b/vendor/phrity/websocket/src/Http/HttpHandler.php new file mode 100644 index 000000000..86081cb1e --- /dev/null +++ b/vendor/phrity/websocket/src/Http/HttpHandler.php @@ -0,0 +1,143 @@ +stream = $stream; + $this->ssl = $ssl; + $this->httpFactory = $httpFactory ?? new DefaultHttpFactory(); + $this->serializer = new Serializer(); + } + + /** + * @deprecated Remove in v4 + */ + public function setLogger(LoggerInterface $logger): void + { + trigger_error('HttpHandler.setLogger is deprecated and will be removed in v4.', E_USER_DEPRECATED); + } + + /** + * @throws RuntimeException + */ + public function pull(): MessageInterface + { + $status = $this->readLine(); + $path = $version = null; + + // Pulling server request + preg_match('!^(?P[A-Z]+) (?P[^ ]*) HTTP/(?P[0-9/.]+)!', $status, $matches); + if (!empty($matches)) { + $path = $matches['path']; + $version = $matches['version']; + $message = $this->httpFactory->createServerRequest($matches['method'], $path); + } + + // Pulling response + preg_match('!^HTTP/(?P[0-9/.]+) (?P[0-9]*)($|\s(?P.*))!', $status, $matches); + if (!empty($matches)) { + $message = $this->httpFactory->createResponse((int)$matches['code'], $matches['reason'] ?? ''); + $version = $matches['version']; + } + + if (empty($message)) { + throw new RuntimeException('Invalid Http request.'); + } + + if ($version) { + $message = $message->withProtocolVersion($version); + } + + while ($header = $this->readLine()) { + $parts = explode(':', $header, 2); + if (count($parts) == 2) { + if ($message->getheaderLine($parts[0]) === '') { + $message = $message->withHeader($parts[0], trim($parts[1])); + } else { + $message = $message->withAddedHeader($parts[0], trim($parts[1])); + } + } + } + if ($message instanceof RequestInterface) { + $scheme = $this->ssl ? 'wss' : 'ws'; + $uri = $this->httpFactory->createUri("{$scheme}://{$message->getHeaderLine('Host')}{$path}"); + $message = $message->withUri($uri, true); + } + + return $message; + } + + /** + * @param MessageInterface $message + * @return MessageInterface + * @throws RuntimeException + */ + public function push(MessageInterface $message): MessageInterface + { + $data = $this->serializer->message($message); + $this->stream->write($data); + return $message; + } + + /** + * @throws RuntimeException + */ + private function readLine(): string + { + $data = ''; + do { + $buffer = $this->stream->readLine(1024); + if (is_null($buffer)) { + throw new RuntimeException('Could not read Http request.'); + } + $data .= $buffer; + } while (!str_ends_with($data, "\n")); + return trim($data); + } +} diff --git a/vendor/phrity/websocket/src/Http/Request.php b/vendor/phrity/websocket/src/Http/Request.php new file mode 100644 index 000000000..15a508953 --- /dev/null +++ b/vendor/phrity/websocket/src/Http/Request.php @@ -0,0 +1,28 @@ +compress; + } + + public function setCompress(bool $compress): void + { + $this->compress = $compress; + } +} diff --git a/vendor/phrity/websocket/src/Message/Close.php b/vendor/phrity/websocket/src/Message/Close.php new file mode 100644 index 000000000..89571d0d4 --- /dev/null +++ b/vendor/phrity/websocket/src/Message/Close.php @@ -0,0 +1,51 @@ +status = $status; + parent::__construct($content); + } + + public function getCloseStatus(): int|null + { + return $this->status; + } + + public function setCloseStatus(int|null $status): void + { + $this->status = $status; + } + + public function getPayload(): string + { + return pack("n", $this->status) . $this->content; + } + + public function setPayload(string $payload = ''): void + { + $this->status = 0; + $this->content = ''; + if (strlen($payload) > 0) { + $this->status = current(unpack('n', $payload) ?: []); + } + if (strlen($payload) > 2) { + $this->content = substr($payload, 2); + } + } +} diff --git a/vendor/phrity/websocket/src/Message/Message.php b/vendor/phrity/websocket/src/Message/Message.php new file mode 100644 index 000000000..27c7b083b --- /dev/null +++ b/vendor/phrity/websocket/src/Message/Message.php @@ -0,0 +1,113 @@ +content = $content; + $this->timestamp = new DateTimeImmutable(); + } + + public function getOpcode(): string + { + return $this->opcode; + } + + public function getLength(): int + { + return strlen($this->content); + } + + public function getTimestamp(): DateTimeInterface + { + return $this->timestamp; + } + + public function getContent(): string + { + return $this->content; + } + + public function setContent(string $content = ''): void + { + $this->content = $content; + } + + public function hasContent(): bool + { + return $this->content != ''; + } + + public function getPayload(): string + { + return $this->content; + } + + public function setPayload(string $payload = ''): void + { + $this->content = $payload; + } + + public function isCompressed(): bool + { + return false; + } + + /** @throws ConnectionFailureException */ + public function setCompress(bool $compress): void + { + if ($compress) { + throw new ConnectionFailureException('Must not compress control message.'); + } + } + + /** + * Split messages into frames + * @param int<1, max> $frameSize + * @return array + */ + public function getFrames(int $frameSize = 4096): array + { + $frames = []; + $split = str_split($this->getPayload(), $frameSize); + if (empty($split)) { + $split = ['']; + } + foreach ($split as $i => $payload) { + $frames[] = new Frame( + $i === 0 ? $this->opcode : 'continuation', + $payload, + $i === array_key_last($split) + ); + } + if ($this->isCompressed()) { + $frames[0]->setRsv1(true); + } + return $frames; + } +} diff --git a/vendor/phrity/websocket/src/Message/MessageHandler.php b/vendor/phrity/websocket/src/Message/MessageHandler.php new file mode 100644 index 000000000..76a2363f3 --- /dev/null +++ b/vendor/phrity/websocket/src/Message/MessageHandler.php @@ -0,0 +1,129 @@ + $frameBuffer */ + private array $frameBuffer = []; + + public function __construct(FrameHandler $frameHandler, Configuration|null $configuration = null) + { + $this->frameHandler = $frameHandler; + $this->initConfiguration($configuration); + } + + /** + * Set logger. + * @param LoggerInterface $logger Logger implementation + * @deprecated Will be removed in future version, set on Configuration instead + */ + public function setLogger(LoggerInterface $logger): void + { + $this->configuration->setLogger($logger); + $this->frameHandler->setLogger($logger); + } + + /** + * Push message + * @template T of Message + * @param T $message + * @param int<1, max> $size + * @return T + */ + public function push(Message $message, int $size = self::DEFAULT_SIZE): Message + { + $frames = $message->getFrames($size); + foreach ($frames as $frame) { + $this->frameHandler->push($frame); + } + $this->configuration->getLogger()->info("[{scope}] Pushed {message}", [ + 'scope' => self::SCOPE, + 'message' => $message, + 'opcode' => $message->getOpcode(), + 'content-length' => $message->getLength(), + 'frames' => count($frames), + ]); + return $message; + } + + // Pull message + public function pull(): Message + { + do { + $frame = $this->frameHandler->pull(); + if ($frame->isFinal()) { + if ($frame->isContinuation()) { + $frames = array_merge($this->frameBuffer, [$frame]); + $this->frameBuffer = []; // Clear buffer + } else { + $frames = [$frame]; + } + return $this->createMessage($frames); + } + // Non-final frame - add to buffer for continuous reading + $this->frameBuffer[] = $frame; + } while (true); + } + + /** + * @param non-empty-array $frames + * @throws BadOpcodeException + */ + private function createMessage(array $frames): Message + { + $opcode = $frames[0]->getOpcode() ?? null; + $message = match ($opcode) { + 'text' => new Text(), + 'binary' => new Binary(), + 'ping' => new Ping(), + 'pong' => new Pong(), + 'close' => new Close(), + default => throw new BadOpcodeException("Invalid opcode '{$opcode}' provided"), + }; + $message->setPayload(array_reduce($frames, function (string $carry, Frame $item) { + return $carry . $item->getPayload(); + }, '')); + $message->setCompress($frames[0]->getRsv1() ?? false); + $this->configuration->getLogger()->info("[{scope}] Pulled {message}", [ + 'scope' => self::SCOPE, + 'message' => $message, + 'opcode' => $message->getOpcode(), + 'content-length' => $message->getLength(), + 'frames' => count($frames), + ]); + return $message; + } +} diff --git a/vendor/phrity/websocket/src/Message/Ping.php b/vendor/phrity/websocket/src/Message/Ping.php new file mode 100644 index 000000000..eb2ae3464 --- /dev/null +++ b/vendor/phrity/websocket/src/Message/Ping.php @@ -0,0 +1,17 @@ +compress; + } + + public function setCompress(bool $compress): void + { + $this->compress = $compress; + } +} diff --git a/vendor/phrity/websocket/src/Middleware/Callback.php b/vendor/phrity/websocket/src/Middleware/Callback.php new file mode 100644 index 000000000..e332d1d5f --- /dev/null +++ b/vendor/phrity/websocket/src/Middleware/Callback.php @@ -0,0 +1,116 @@ +incoming = $incoming; + $this->outgoing = $outgoing; + $this->httpIncoming = $httpIncoming; + $this->httpOutgoing = $httpOutgoing; + $this->tick = $tick; + $this->initConfiguration(); + } + + /** + * Set logger. + * @param LoggerInterface $logger + * @deprecated Will be removed in future version, retrieved from Configuration instead + */ + public function setLogger(LoggerInterface $logger): void + { + $this->configuration->setLogger($logger); + } + + public function processIncoming(ProcessStack $stack, Connection $connection): Message + { + if (is_callable($this->incoming)) { + return call_user_func($this->incoming, $stack, $connection); + } + return $stack->handleIncoming(); + } + + public function processOutgoing(ProcessStack $stack, Connection $connection, Message $message): Message + { + if (is_callable($this->outgoing)) { + return call_user_func($this->outgoing, $stack, $connection, $message); + } + return $stack->handleOutgoing($message); + } + + public function processHttpIncoming(ProcessHttpStack $stack, Connection $connection): MessageInterface + { + if (is_callable($this->httpIncoming)) { + return call_user_func($this->httpIncoming, $stack, $connection); + } + return $stack->handleHttpIncoming(); + } + + public function processHttpOutgoing( + ProcessHttpStack $stack, + Connection $connection, + MessageInterface $message + ): MessageInterface { + if (is_callable($this->httpOutgoing)) { + return call_user_func($this->httpOutgoing, $stack, $connection, $message); + } + return $stack->handleHttpOutgoing($message); + } + + public function processTick(ProcessTickStack $stack, Connection $connection): void + { + if (is_callable($this->tick)) { + call_user_func($this->tick, $stack, $connection); + } + $stack->handleTick(); + } +} diff --git a/vendor/phrity/websocket/src/Middleware/CloseHandler.php b/vendor/phrity/websocket/src/Middleware/CloseHandler.php new file mode 100644 index 000000000..b523d563a --- /dev/null +++ b/vendor/phrity/websocket/src/Middleware/CloseHandler.php @@ -0,0 +1,107 @@ +initConfiguration(); + } + + /** + * Set logger. + * @param LoggerInterface $logger + * @deprecated Will be removed in future version, retrieved from Configuration instead + */ + public function setLogger(LoggerInterface $logger): void + { + $this->configuration->setLogger($logger); + } + + public function processIncoming(ProcessStack $stack, Connection $connection): Message + { + $message = $stack->handleIncoming(); // Proceed before logic + if (!$message instanceof Close) { + return $message; + } + if ($connection->isWritable()) { + // Remote sent Close; acknowledge and close for further reading + $this->configuration->getLogger()->debug("[{scope}] Received 'close', status: {status}", [ + 'scope' => self::SCOPE, + 'connection' => $connection->getIdentity(), + 'status' => $message->getCloseStatus(), + ]); + $ack = "Close acknowledged: {$message->getCloseStatus()}"; + $connection->closeRead(); + $connection->send(new Close($message->getCloseStatus(), $ack)); + } else { + // Remote sent Close/Ack: disconnect + $this->configuration->getLogger()->debug("[{scope}] Received 'close' acknowledge, disconnecting", [ + 'scope' => self::SCOPE, + 'connection' => $connection->getIdentity(), + 'status' => $message->getCloseStatus(), + ]); + $connection->disconnect(); + } + return $message; + } + + public function processOutgoing(ProcessStack $stack, Connection $connection, Message $message): Message + { + $message = $stack->handleOutgoing($message); // Proceed before logic + if (!$message instanceof Close) { + return $message; + } + if ($connection->isReadable()) { + // Local sent Close: close for further writing, expect remote acknowledge + $this->configuration->getLogger()->debug("[{scope}] Sent 'close', status: {status}", [ + 'scope' => self::SCOPE, + 'connection' => $connection->getIdentity(), + 'status' => $message->getCloseStatus(), + ]); + $connection->closeWrite(); + } else { + // Local sent Close/Ack: disconnect + $this->configuration->getLogger()->debug("[{scope}] Sent 'close' acknowledge, disconnecting", [ + 'scope' => self::SCOPE, + 'connection' => $connection->getIdentity(), + 'status' => $message->getCloseStatus(), + ]); + $connection->disconnect(); + } + return $message; + } +} diff --git a/vendor/phrity/websocket/src/Middleware/CompressionExtension.php b/vendor/phrity/websocket/src/Middleware/CompressionExtension.php new file mode 100644 index 000000000..6e7fb3505 --- /dev/null +++ b/vendor/phrity/websocket/src/Middleware/CompressionExtension.php @@ -0,0 +1,185 @@ + $compressors */ + private array $compressors = []; + + public function __construct(CompressorInterface ...$compressors) + { + $this->compressors = $compressors; + $this->initConfiguration(); + } + + /** + * Set logger. + * @param LoggerInterface $logger + * @deprecated Will be removed in future version, retrieved from Configuration instead + */ + public function setLogger(LoggerInterface $logger): void + { + $this->configuration->setLogger($logger); + } + + public function processHttpOutgoing( + ProcessHttpStack $stack, + Connection $connection, + MessageInterface $message + ): MessageInterface { + if ($message instanceof RequestInterface) { + // Outgoing requests on Client + $connection->setMeta('compressionExtension.compressor', null); + $connection->setMeta('compressionExtension.configuration', null); + $headerValues = []; + foreach ($this->compressors as $compressor) { + $headerValues[] = $compressor->getRequestHeaderValue(); + } + $message = $message->withAddedHeader('Sec-WebSocket-Extensions', implode(', ', $headerValues)); + } elseif ($message instanceof ResponseInterface) { + // Outgoing Response on Server + if ($compressor = $connection->getMeta('compressionExtension.compressor')) { + $configuration = $connection->getMeta('compressionExtension.configuration'); + $message = $message->withHeader( + 'Sec-WebSocket-Extensions', + $compressor->getResponseHeaderValue($configuration) + ); + } + } + return $stack->handleHttpOutgoing($message); + } + + public function processHttpIncoming(ProcessHttpStack $stack, Connection $connection): MessageInterface + { + $message = $stack->handleHttpIncoming(); + if ($message instanceof ServerRequestInterface) { + // Incoming requests on Server + $connection->setMeta('compressionExtension.compressor', null); + $connection->setMeta('compressionExtension.configuration', null); + if ($preferred = $this->getPreferred($message)) { + $connection->setMeta('compressionExtension.compressor', $preferred->compressor); + $connection->setMeta('compressionExtension.configuration', $preferred->configuration); + $this->configuration->getLogger()->debug("[{scope}] Using: {compressor}", [ + 'scope' => self::SCOPE, + 'connection' => $connection->getIdentity(), + 'compressor' => $preferred->compressor, + 'configuration' => (array)$preferred->configuration, + ]); + } + } elseif ($message instanceof ResponseInterface) { + // Incoming Response on Client + if ($preferred = $this->getPreferred($message)) { + $connection->setMeta('compressionExtension.compressor', $preferred->compressor); + $connection->setMeta('compressionExtension.configuration', $preferred->configuration); + $this->configuration->getLogger()->debug("[{scope}] Using: {compressor}", [ + 'scope' => self::SCOPE, + 'connection' => $connection->getIdentity(), + 'compressor' => $preferred->compressor, + 'configuration' => (array)$preferred->configuration, + ]); + } + // @todo: If not found? + } + return $message; + } + + public function processIncoming(ProcessStack $stack, Connection $connection): Message + { + $message = $stack->handleIncoming(); + if ( + ($message instanceof Text || $message instanceof Binary) + && $message->isCompressed() + && $compressor = $connection->getMeta('compressionExtension.compressor') + ) { + $message = $compressor->decompress($message, $connection->getMeta('compressionExtension.configuration')); + } + return $message; + } + + /** + * @template T of Message + * @param T $message + * @return T|Text|Binary + */ + public function processOutgoing(ProcessStack $stack, Connection $connection, Message $message): Message + { + if ( + ($message instanceof Text || $message instanceof Binary) + && !$message->isCompressed() + && $compressor = $connection->getMeta('compressionExtension.compressor') + ) { + /** @var Text|Binary $message */ + $message = $compressor->compress($message, $connection->getMeta('compressionExtension.configuration')); + } + return $stack->handleOutgoing($message); + } + + /** + * @return object{compressor: CompressorInterface, configuration: object}|null + */ + protected function getPreferred(MessageInterface $request): object|null + { + $isServer = $request instanceof ServerRequestInterface; + foreach ($request->getHeader('Sec-WebSocket-Extensions') as $header) { + foreach (explode(',', $header) as $element) { + foreach ($this->compressors as $compressor) { + $configuration = $compressor->getConfiguration(trim($element), $isServer); + if ($compressor->isEligable($configuration)) { + return (object)['compressor' => $compressor, 'configuration' => $configuration]; + } + } + } + } + return null; + } +} diff --git a/vendor/phrity/websocket/src/Middleware/CompressionExtension/CompressorInterface.php b/vendor/phrity/websocket/src/Middleware/CompressionExtension/CompressorInterface.php new file mode 100644 index 000000000..442995e78 --- /dev/null +++ b/vendor/phrity/websocket/src/Middleware/CompressionExtension/CompressorInterface.php @@ -0,0 +1,39 @@ +, + * clientMaxWindowBits: int, + * } + */ +class DeflateCompressor implements CompressorInterface, Stringable +{ + use StringableTrait; + + private const MIN_WINDOW_SIZE = 9; + private const MAX_WINDOW_SIZE = 15; + + private bool $serverNoContextTakeover; + private bool $clientNoContextTakeover; + /** @var int $serverMaxWindowBits */ + private int $serverMaxWindowBits; + /** @var int $clientMaxWindowBits */ + private int $clientMaxWindowBits; + + /** + * @throws RuntimeException + * @throws RangeException + */ + public function __construct( + bool $serverNoContextTakeover = false, + bool $clientNoContextTakeover = false, + int $serverMaxWindowBits = self::MAX_WINDOW_SIZE, + int $clientMaxWindowBits = self::MAX_WINDOW_SIZE, + string $extension = 'zlib', + ) { + if (!extension_loaded($extension)) { + throw new RuntimeException("DeflateCompressor require {$extension} extension."); + } + if ($serverMaxWindowBits < self::MIN_WINDOW_SIZE || $serverMaxWindowBits > self::MAX_WINDOW_SIZE) { + throw new RangeException("DeflateCompressor serverMaxWindowBits must be in range 9-15."); + } + if ($clientMaxWindowBits < self::MIN_WINDOW_SIZE || $clientMaxWindowBits > self::MAX_WINDOW_SIZE) { + throw new RangeException("DeflateCompressor clientMaxWindowBits must be in range 9-15."); + } + $this->serverNoContextTakeover = $serverNoContextTakeover; + $this->clientNoContextTakeover = $clientNoContextTakeover; + $this->serverMaxWindowBits = $serverMaxWindowBits; + $this->clientMaxWindowBits = $clientMaxWindowBits; + } + + public function getRequestHeaderValue(): string + { + $header = "permessage-deflate"; + if ($this->serverNoContextTakeover) { + $header .= "; server_no_context_takeover"; + } + if ($this->clientNoContextTakeover) { + $header .= "; client_no_context_takeover"; + } + if ($this->serverMaxWindowBits != self::MAX_WINDOW_SIZE) { + $header .= "; server_max_window_bits={$this->serverMaxWindowBits}"; + } + if ($this->clientMaxWindowBits != self::MAX_WINDOW_SIZE) { + $header .= "; client_max_window_bits={$this->clientMaxWindowBits}"; + } + return $header; + } + + /** + * @param Config $configuration + */ + public function getResponseHeaderValue(object $configuration): string + { + // @todo: throw HandshakeException or bad config + $header = "permessage-deflate"; + if ($configuration->serverNoContextTakeover) { + $header .= "; server_no_context_takeover"; + } + if ($configuration->clientNoContextTakeover) { + $header .= "; client_no_context_takeover"; + } + $serverMaxWindowBits = min($configuration->serverMaxWindowBits, $this->serverMaxWindowBits); + if ($serverMaxWindowBits != self::MAX_WINDOW_SIZE) { + $header .= "; server_max_window_bits={$serverMaxWindowBits}"; + } + $clientMaxWindowBits = min($configuration->clientMaxWindowBits, $this->clientMaxWindowBits); + if ($clientMaxWindowBits != self::MAX_WINDOW_SIZE) { + $header .= "; client_max_window_bits={$clientMaxWindowBits}"; + } + return $header; + } + + /** + * @param Config $configuration + */ + public function isEligable(object $configuration): bool + { + return + $configuration->compressor == 'permessage-deflate' + && $configuration->serverMaxWindowBits <= $this->serverMaxWindowBits + && $configuration->clientMaxWindowBits <= $this->clientMaxWindowBits + ; + } + + /** + * @return Config + */ + public function getConfiguration(string $element, bool $isServer): object + { + $configuration = (object)[ + 'compressor' => null, + 'isServer' => $isServer, + 'serverNoContextTakeover' => $this->serverNoContextTakeover, + 'clientNoContextTakeover' => $this->clientNoContextTakeover, + 'serverMaxWindowBits' => $this->serverMaxWindowBits, + 'clientMaxWindowBits' => $this->clientMaxWindowBits, + 'deflator' => null, + 'inflator' => null, + ]; + foreach (explode(';', $element) as $parameter) { + $parts = explode('=', $parameter); + $key = trim(array_shift($parts)); + $value = array_shift($parts); + // @todo: Error handling when parsing + switch ($key) { + case 'permessage-deflate': + $configuration->compressor = $key; + break; + case 'server_no_context_takeover': + $configuration->serverNoContextTakeover = true; + break; + case 'client_no_context_takeover': + $configuration->clientNoContextTakeover = true; + break; + case 'server_max_window_bits': + $bits = $this->intVal($value); + $configuration->serverMaxWindowBits = min($bits, $this->serverMaxWindowBits); + break; + case 'client_max_window_bits': + $bits = $this->intVal($value); + $configuration->clientMaxWindowBits = min($bits, $this->clientMaxWindowBits); + break; + } + } + return $configuration; + } + + /** + * @template T of Binary|Text + * @param T $message + * @param Config $configuration + * @return T + */ + public function compress(Binary|Text $message, object $configuration): Binary|Text + { + $windowBits = $configuration->isServer + ? $configuration->serverMaxWindowBits + : $configuration->clientMaxWindowBits; + $noContextTakeover = $configuration->isServer + ? $configuration->serverNoContextTakeover + : $configuration->clientNoContextTakeover; + + if (is_null($configuration->deflator) || $noContextTakeover) { + $configuration->deflator = deflate_init(ZLIB_ENCODING_RAW, [ + 'level' => -1, + 'window' => $windowBits, + 'strategy' => ZLIB_DEFAULT_STRATEGY + ]) ?: null; + } + /** @var DeflateContext $deflator */ + $deflator = $configuration->deflator; + /** @var string $deflated */ + $deflated = deflate_add($deflator, $message->getPayload(), ZLIB_SYNC_FLUSH); + $deflated = substr($deflated, 0, -4); // Remove 4 last chars + $message->setCompress(true); + $message->setPayload($deflated); + return $message; + } + + /** + * @param Config $configuration + */ + public function decompress(Binary|Text $message, object $configuration): Binary|Text + { + $windowBits = $configuration->isServer + ? $configuration->clientMaxWindowBits + : $configuration->serverMaxWindowBits; + $noContextTakeover = $configuration->isServer + ? $configuration->clientNoContextTakeover + : $configuration->serverNoContextTakeover; + + if (is_null($configuration->inflator) || $noContextTakeover) { + $configuration->inflator = inflate_init(ZLIB_ENCODING_RAW, [ + 'level' => -1, + 'window' => $windowBits, + 'strategy' => ZLIB_DEFAULT_STRATEGY + ]) ?: null; + } + /** @var InflateContext $inflator */ + $inflator = $configuration->inflator; + /** @var string $inflated */ + $inflated = inflate_add($inflator, $message->getPayload() . "\x00\x00\xff\xff"); + $message->setCompress(false); + $message->setPayload($inflated); + return $message; + } + + private function intVal(string|null $input): int + { + if (is_null($input)) { + return self::MAX_WINDOW_SIZE; + } + preg_match('/([1-9][0-9]*)/', $input, $matches); + if (empty($matches)) { + return self::MAX_WINDOW_SIZE; + } + $value = (int)array_shift($matches); + return min(max($value, self::MIN_WINDOW_SIZE), self::MAX_WINDOW_SIZE); + } +} diff --git a/vendor/phrity/websocket/src/Middleware/FollowRedirect.php b/vendor/phrity/websocket/src/Middleware/FollowRedirect.php new file mode 100644 index 000000000..68302090d --- /dev/null +++ b/vendor/phrity/websocket/src/Middleware/FollowRedirect.php @@ -0,0 +1,96 @@ +limit = $limit; + $this->initConfiguration(); + } + + /** + * Set logger. + * @param LoggerInterface $logger + * @deprecated Will be removed in future version, retrieved from Configuration instead + */ + public function setLogger(LoggerInterface $logger): void + { + $this->configuration->setLogger($logger); + } + + /** + * @throws HandshakeException + * @throws ReconnectException + */ + public function processHttpIncoming(ProcessHttpStack $stack, Connection $connection): MessageInterface + { + $message = $stack->handleHttpIncoming(); + if ( + $message instanceof ResponseInterface + && $message->getStatusCode() >= 300 + && $message->getStatusCode() < 400 + && $locationHeader = $message->getHeaderLine('Location') + ) { + $context = [ + 'scope' => self::SCOPE, + 'connection' => $connection->getIdentity(), + 'attempts' => $this->attempts, + 'limit' => $this->limit, + 'status' => $message->getStatusCode(), + 'location' => $locationHeader, + ]; + if ($this->attempts > $this->limit) { + $this->configuration->getLogger()->warning("[{scope}] Too many redirect attempts, giving up", $context); + throw new HandshakeException("Too many redirect attempts, giving up", $message); + } + $this->attempts++; + $this->configuration->getLogger()->info("[{scope}] Redirect {status} to {location}", $context); + throw new ReconnectException(new Uri($locationHeader)); + } + return $message; + } +} diff --git a/vendor/phrity/websocket/src/Middleware/MiddlewareHandler.php b/vendor/phrity/websocket/src/Middleware/MiddlewareHandler.php new file mode 100644 index 000000000..c7770df30 --- /dev/null +++ b/vendor/phrity/websocket/src/Middleware/MiddlewareHandler.php @@ -0,0 +1,204 @@ + */ + private array $middlewares = []; + /** @var array */ + private array $incoming = []; + /** @var array */ + private array $outgoing = []; + /** @var array */ + private array $httpIncoming = []; + /** @var array */ + private array $httpOutgoing = []; + /** @var array */ + private array $tick = []; + + // Handlers + private HttpHandler $httpHandler; + private MessageHandler $messageHandler; + + /** + * Create MiddlewareHandler. + * @param MessageHandler $messageHandler + * @param HttpHandler $httpHandler + */ + public function __construct( + MessageHandler $messageHandler, + HttpHandler $httpHandler, + Configuration|null $configuration = null, + ) { + $this->messageHandler = $messageHandler; + $this->httpHandler = $httpHandler; + $this->initConfiguration($configuration); + } + + /** + * Set logger on MiddlewareHandler and all LoggerAware middlewares. + * @param LoggerInterface $logger + * @deprecated Will be removed in future version, set on Configuration instead + */ + public function setLogger(LoggerInterface $logger): void + { + $this->configuration->setLogger($logger); + foreach ($this->middlewares as $middleware) { + if ($middleware instanceof LoggerAwareInterface) { + $middleware->setLogger($logger); + } + } + } + + /** + * Add a middleware. + * @param MiddlewareInterface $middleware + * @return $this + */ + public function add(MiddlewareInterface $middleware): self + { + $context = [ + 'scope' => self::SCOPE, + 'middleware' => $middleware, + ]; + if ($middleware instanceof ProcessIncomingInterface) { + $this->configuration->getLogger()->info("[{scope}] Added incoming: {middleware}", $context); + $this->incoming[] = $middleware; + } + if ($middleware instanceof ProcessOutgoingInterface) { + $this->configuration->getLogger()->info("[{scope}] Added outgoing: {middleware}", $context); + $this->outgoing[] = $middleware; + } + if ($middleware instanceof ProcessHttpIncomingInterface) { + $this->configuration->getLogger()->info("[{scope}] Added http incoming: {middleware}", $context); + $this->httpIncoming[] = $middleware; + } + if ($middleware instanceof ProcessHttpOutgoingInterface) { + $this->configuration->getLogger()->info("[{scope}] Added http outgoing: {middleware}", $context); + $this->httpOutgoing[] = $middleware; + } + if ($middleware instanceof ProcessTickInterface) { + $this->configuration->getLogger()->info("[{scope}] Added tick: {middleware}", $context); + $this->tick[] = $middleware; + } + if ($middleware instanceof LoggerAwareInterface) { + $middleware->setLogger($this->configuration->getLogger()); + } + $this->middlewares[] = $middleware; + return $this; + } + + /** + * Process middlewares for incoming messages. + * @param Connection $connection + * @return Message + */ + public function processIncoming(Connection $connection): Message + { + $this->configuration->getLogger()->info("[{scope}] Processing incoming", [ + 'scope' => self::SCOPE, + 'connection' => $connection->getIdentity(), + ]); + $stack = new ProcessStack($connection, $this->messageHandler, $this->incoming); + return $stack->handleIncoming(); + } + + /** + * Process middlewares for outgoing messages. + * @template T of Message + * @param Connection $connection + * @param T $message + * @return T + */ + public function processOutgoing(Connection $connection, Message $message): Message + { + $this->configuration->getLogger()->info("[{scope}] Processing outgoing", [ + 'scope' => self::SCOPE, + 'connection' => $connection->getIdentity(), + ]); + $stack = new ProcessStack($connection, $this->messageHandler, $this->outgoing); + return $stack->handleOutgoing($message); + } + + /** + * Process middlewares for http requests. + * @param Connection $connection + * @return MessageInterface + */ + public function processHttpIncoming(Connection $connection): MessageInterface + { + $this->configuration->getLogger()->info("[{scope}] Processing http incoming", [ + 'scope' => self::SCOPE, + 'connection' => $connection->getIdentity(), + ]); + $stack = new ProcessHttpStack($connection, $this->httpHandler, $this->httpIncoming); + return $stack->handleHttpIncoming(); + } + + /** + * Process middlewares for http requests. + * @param Connection $connection + * @param MessageInterface $message + * @return MessageInterface + */ + public function processHttpOutgoing(Connection $connection, MessageInterface $message): MessageInterface + { + $this->configuration->getLogger()->info("[{scope}] Processing http outgoing", [ + 'scope' => self::SCOPE, + 'connection' => $connection->getIdentity(), + ]); + $stack = new ProcessHttpStack($connection, $this->httpHandler, $this->httpOutgoing); + return $stack->handleHttpOutgoing($message); + } + + /** + * Process middlewares for tick. + * @param Connection $connection + */ + public function processTick(Connection $connection): void + { + $this->configuration->getLogger()->info("[{scope}] Processing tick", [ + 'scope' => self::SCOPE, + 'connection' => $connection->getIdentity(), + ]); + $stack = new ProcessTickStack($connection, $this->tick); + $stack->handleTick(); + } +} diff --git a/vendor/phrity/websocket/src/Middleware/MiddlewareInterface.php b/vendor/phrity/websocket/src/Middleware/MiddlewareInterface.php new file mode 100644 index 000000000..c7e7a92ae --- /dev/null +++ b/vendor/phrity/websocket/src/Middleware/MiddlewareInterface.php @@ -0,0 +1,18 @@ +interval = $interval; + $this->initConfiguration(); + } + + /** + * Set logger. + * @param LoggerInterface $logger + * @deprecated Will be removed in future version, retrieved from Configuration instead + */ + public function setLogger(LoggerInterface $logger): void + { + $this->configuration->setLogger($logger); + } + + public function processOutgoing(ProcessStack $stack, Connection $connection, Message $message): Message + { + $this->setNext($connection); // Update timestamp for next ping + return $stack->handleOutgoing($message); + } + + public function processTick(ProcessTickStack $stack, Connection $connection): void + { + // Push if time exceeds timestamp for next ping + if ($connection->isWritable() && microtime(true) >= $this->getNext($connection)) { + $this->configuration->getLogger()->debug("[{scope}] Auto-pushing ping", [ + 'scope' => self::SCOPE, + 'connection' => $connection->getIdentity(), + ]); + $connection->send(new Ping()); + $this->setNext($connection); // Update timestamp for next ping + } + $stack->handleTick(); + } + + private function getNext(Connection $connection): float + { + return $connection->getMeta('pingInterval.next') ?? $this->setNext($connection); + } + + private function setNext(Connection $connection): float + { + $next = microtime(true) + ($this->interval ?? $connection->getTimeout()); + $connection->setMeta('pingInterval.next', $next); + return $next; + } +} diff --git a/vendor/phrity/websocket/src/Middleware/PingResponder.php b/vendor/phrity/websocket/src/Middleware/PingResponder.php new file mode 100644 index 000000000..b8704a32c --- /dev/null +++ b/vendor/phrity/websocket/src/Middleware/PingResponder.php @@ -0,0 +1,61 @@ +initConfiguration(); + } + + /** + * Set logger. + * @param LoggerInterface $logger + * @deprecated Will be removed in future version, retrieved from Configuration instead + */ + public function setLogger(LoggerInterface $logger): void + { + $this->configuration->setLogger($logger); + } + + public function processIncoming(ProcessStack $stack, Connection $connection): Message + { + $message = $stack->handleIncoming(); + if ($message instanceof Ping && $connection->isWritable()) { + $connection->send(new Pong($message->getContent())); + } + return $message; + } +} diff --git a/vendor/phrity/websocket/src/Middleware/ProcessHttpIncomingInterface.php b/vendor/phrity/websocket/src/Middleware/ProcessHttpIncomingInterface.php new file mode 100644 index 000000000..394bd11b3 --- /dev/null +++ b/vendor/phrity/websocket/src/Middleware/ProcessHttpIncomingInterface.php @@ -0,0 +1,20 @@ + $processors */ + private array $processors; + + /** + * Create ProcessStack. + * @param Connection $connection + * @param HttpHandler $httpHandler + * @param array $processors + */ + public function __construct(Connection $connection, HttpHandler $httpHandler, array $processors) + { + $this->connection = $connection; + $this->httpHandler = $httpHandler; + $this->processors = $processors; + } + + /** + * Process middleware for incoming http message. + * @return MessageInterface + */ + public function handleHttpIncoming(): MessageInterface + { + /** @var ProcessHttpIncomingInterface|null $processor */ + $processor = array_shift($this->processors); + if ($processor) { + return $processor->processHttpIncoming($this, $this->connection); + } + return $this->httpHandler->pull(); + } + + /** + * Process middleware for outgoing http message. + * @param MessageInterface $message + * @return MessageInterface + */ + public function handleHttpOutgoing(MessageInterface $message): MessageInterface + { + /** @var ProcessHttpOutgoingInterface|null $processor */ + $processor = array_shift($this->processors); + if ($processor) { + return $processor->processHttpOutgoing($this, $this->connection, $message); + } + return $this->httpHandler->push($message); + } +} diff --git a/vendor/phrity/websocket/src/Middleware/ProcessIncomingInterface.php b/vendor/phrity/websocket/src/Middleware/ProcessIncomingInterface.php new file mode 100644 index 000000000..4c895e8f9 --- /dev/null +++ b/vendor/phrity/websocket/src/Middleware/ProcessIncomingInterface.php @@ -0,0 +1,20 @@ + $processors */ + private array $processors; + + /** + * Create ProcessStack. + * @param Connection $connection + * @param MessageHandler $messageHandler + * @param array $processors + */ + public function __construct(Connection $connection, MessageHandler $messageHandler, array $processors) + { + $this->connection = $connection; + $this->messageHandler = $messageHandler; + $this->processors = $processors; + } + + /** + * Process middleware for incoming message. + * @return Message + */ + public function handleIncoming(): Message + { + /** @var ProcessIncomingInterface|null $processor */ + $processor = array_shift($this->processors); + if ($processor) { + return $processor->processIncoming($this, $this->connection); + } + return $this->messageHandler->pull(); + } + + /** + * Process middleware for outgoing message. + * @template T of Message + * @param T $message + * @return T + */ + public function handleOutgoing(Message $message): Message + { + /** @var ProcessOutgoingInterface|null $processor */ + $processor = array_shift($this->processors); + if ($processor) { + return $processor->processOutgoing($this, $this->connection, $message); + } + return $this->messageHandler->push($message, $this->connection->getConfiguration()->getFrameSize()); + } +} diff --git a/vendor/phrity/websocket/src/Middleware/ProcessTickInterface.php b/vendor/phrity/websocket/src/Middleware/ProcessTickInterface.php new file mode 100644 index 000000000..04256c65e --- /dev/null +++ b/vendor/phrity/websocket/src/Middleware/ProcessTickInterface.php @@ -0,0 +1,19 @@ + $processors */ + private array $processors; + + /** + * Create ProcessStack. + * @param Connection $connection + * @param array $processors + */ + public function __construct(Connection $connection, array $processors) + { + $this->connection = $connection; + $this->processors = $processors; + } + + /** + * Process middleware for tick. + */ + public function handleTick(): void + { + $processor = array_shift($this->processors); + if ($processor) { + $processor->processTick($this, $this->connection); + } + } +} diff --git a/vendor/phrity/websocket/src/Middleware/SubprotocolNegotiation.php b/vendor/phrity/websocket/src/Middleware/SubprotocolNegotiation.php new file mode 100644 index 000000000..8f8be18b8 --- /dev/null +++ b/vendor/phrity/websocket/src/Middleware/SubprotocolNegotiation.php @@ -0,0 +1,151 @@ + $subprotocols */ + private array $subprotocols; + private bool $require; + + /** @param array $subprotocols */ + public function __construct(array $subprotocols, bool $require = false) + { + $this->subprotocols = $subprotocols; + $this->require = $require; + $this->initConfiguration(); + } + + /** + * Set logger. + * @param LoggerInterface $logger + * @deprecated Will be removed in future version, retrieved from Configuration instead + */ + public function setLogger(LoggerInterface $logger): void + { + $this->configuration->setLogger($logger); + } + + public function processHttpOutgoing( + ProcessHttpStack $stack, + Connection $connection, + MessageInterface $message + ): MessageInterface { + if ($message instanceof RequestInterface) { + // Outgoing requests on Client + foreach ($this->subprotocols as $subprotocol) { + $message = $message->withAddedHeader('Sec-WebSocket-Protocol', $subprotocol); + } + if ($requested = implode(', ', $this->subprotocols)) { + $this->configuration->getLogger()->debug("[{scope}] Requested subprotocols: {requested}", [ + 'scope' => self::SCOPE, + 'connection' => $connection->getIdentity(), + 'requested' => $requested, + ]); + } + } elseif ($message instanceof ResponseInterface) { + // Outgoing Response on Server + if ($selected = $connection->getMeta('subprotocolNegotiation.selected')) { + $message = $message->withHeader('Sec-WebSocket-Protocol', $selected); + $this->configuration->getLogger()->info("[{scope}] Selected subprotocol: {selected}", [ + 'scope' => self::SCOPE, + 'connection' => $connection->getIdentity(), + 'selected' => $selected, + ]); + } elseif ($this->require) { + // No matching subprotocol, fail handshake + $message = $message->withStatus(426); + } + } + return $stack->handleHttpOutgoing($message); + } + + /** + * @throws HandshakeException + */ + public function processHttpIncoming(ProcessHttpStack $stack, Connection $connection): MessageInterface + { + $connection->setMeta('subprotocolNegotiation.selected', null); + $message = $stack->handleHttpIncoming(); + + if ($message instanceof ServerRequestInterface) { + // Incoming requests on Server + if ($requested = $message->getHeaderLine('Sec-WebSocket-Protocol')) { + $this->configuration->getLogger()->debug("[{scope}] Requested subprotocols: {requested}", [ + 'scope' => self::SCOPE, + 'connection' => $connection->getIdentity(), + 'requested' => $requested, + ]); + } + if ($supported = implode(', ', $this->subprotocols)) { + $this->configuration->getLogger()->debug("[{scope}] Supported subprotocols: {supported}", [ + 'scope' => self::SCOPE, + 'connection' => $connection->getIdentity(), + 'supported' => $supported, + ]); + } + foreach ($message->getHeader('Sec-WebSocket-Protocol') as $subprotocol) { + if (in_array($subprotocol, $this->subprotocols)) { + $connection->setMeta('subprotocolNegotiation.selected', $subprotocol); + return $message; + } + } + } elseif ($message instanceof ResponseInterface) { + // Incoming Response on Client + if ($selected = $message->getHeaderLine('Sec-WebSocket-Protocol')) { + $connection->setMeta('subprotocolNegotiation.selected', $selected); + $this->configuration->getLogger()->info("[{scope}] Selected subprotocol: {selected}", [ + 'scope' => self::SCOPE, + 'connection' => $connection->getIdentity(), + 'selected' => $selected, + ]); + } elseif ($this->require) { + // No matching subprotocol, close and fail + $connection->close(); + throw new HandshakeException("Could not resolve subprotocol.", $message); + } + } + return $message; + } +} diff --git a/vendor/phrity/websocket/src/Runtime/IdentityInterface.php b/vendor/phrity/websocket/src/Runtime/IdentityInterface.php new file mode 100644 index 000000000..40565bc52 --- /dev/null +++ b/vendor/phrity/websocket/src/Runtime/IdentityInterface.php @@ -0,0 +1,19 @@ + $containers */ + private array $containers = []; + + public function __construct(StreamFactory $streamFactory) + { + $this->streamFactory = $streamFactory; + $this->streamCollection = $this->streamFactory->createStreamCollection(); + } + + public function attach(StreamContainerInterface $streamContainer, Closure $onSelect, string $identity): void + { + if (array_key_exists($identity, $this->containers)) { + // On repeated identity, check if actually readable (detach if not) + if ($this->containers[$identity]->stream->isReadable()) { + throw new RunnerException("Stream container with identity {$identity} already attached"); + } + $this->detach($identity); + } + $stream = $streamContainer->getStream(); + $this->streamCollection->attach($stream, $identity); + $this->containers[$identity] = (object)[ + 'container' => $streamContainer, + 'stream' => $stream, + 'onSelect' => $onSelect, + ]; + } + + public function detach(string $identity): void + { + if (array_key_exists($identity, $this->containers)) { + $this->streamCollection->detach($identity); + unset($this->containers[$identity]); + } + } + + /** + * @throws ExceptionInterface + */ + public function handle(int|float $timeout): void + { + foreach ($this->select($timeout) as $identity => $stream) { + $container = $this->containers[$identity]; + /** @throws ExceptionInterface */ + call_user_func($container->onSelect, $this, $container->container); + } + } + + public function select(int|float $timeout): StreamCollection + { + return $this->streamCollection->waitRead($timeout); + } +} diff --git a/vendor/phrity/websocket/src/Server.php b/vendor/phrity/websocket/src/Server.php new file mode 100644 index 000000000..7d07b29dd --- /dev/null +++ b/vendor/phrity/websocket/src/Server.php @@ -0,0 +1,761 @@ + */ + use ListenerTrait; + use SendMethodsTrait; + use StringableTrait; + + private const SCOPE = 'server'; + + // Settings + private int $port; + private string $scheme; + + // Internal resources + private StreamFactory $streamFactory; + private SocketServer|null $server = null; + private Runner $runner; + + private bool $running = false; + /** @var array $connections */ + private array $connections = []; + /** @var array $middlewares */ + private array $middlewares = []; + private bool $allowConnections = false; + private HttpFactory $httpFactory; + /** @var non-empty-string $identity */ + private string $identity; + + + /* ---------- Magic methods ------------------------------------------------------------------------------------ */ + + /** + * @param int $port Socket port to listen to + * @param bool $ssl If SSL should be used + * @param Configuration|null $configuration + * @param StreamFactory|null $streamFactory + * @throws InvalidArgumentException If invalid port provided + */ + public function __construct( + int $port = 80, + bool $ssl = false, + Configuration|null $configuration = null, + StreamFactory|null $streamFactory = null, + ) { + if ($port < 0 || $port > 65535) { + throw new InvalidArgumentException("Invalid port '{$port}' provided"); + } + $this->port = $port; + $this->scheme = $ssl ? 'ssl' : 'tcp'; + $this->httpFactory = new DefaultHttpFactory(); + $this->streamFactory = $streamFactory ?? new StreamFactory(); + $this->identity = "server/{$port}"; + $this->initConfiguration($configuration); + $this->runner = new Runner($this->streamFactory); + } + + /** + * Get string representation of instance. + * @return string String representation + */ + public function __toString(): string + { + return $this->stringable('%s', $this->server ? "{$this->scheme}://0.0.0.0:{$this->port}" : 'closed'); + } + + + /* ---------- Configuration ------------------------------------------------------------------------------------ */ + + public function getIdentity(): string + { + return $this->identity; + } + + /** + * Set stream factory to use. + * @param StreamFactory $streamFactory + * @return self + * @depracated Remove in v4 + */ + public function setStreamFactory(StreamFactory $streamFactory): self + { + trigger_error('Server.setStreamFactory is deprecated and will be removed in v4.', E_USER_DEPRECATED); + $this->streamFactory = $streamFactory; + return $this; + } + + /** + * Set HTTP factory to use. + * @param HttpFactory $httpFactory + * @return self + */ + public function setHttpFactory(HttpFactory $httpFactory): self + { + $this->httpFactory = $httpFactory; + return $this; + } + + /** + * Set logger. + * @param LoggerInterface $logger Logger implementation + * @deprecated Will be removed in future version, set on Configuration instead + */ + public function setLogger(LoggerInterface $logger): void + { + $this->configuration->setLogger($logger); + foreach ($this->connections as $connection) { + $connection->setLogger($logger); + } + } + + /** + * Set timeout. + * @param int<0, max>|float $timeout Timeout in seconds + * @return self + * @throws InvalidArgumentException If invalid timeout provided + * @deprecated Will be removed in future version, set on Configuration instead + */ + public function setTimeout(int|float $timeout): self + { + $this->configuration->setTimeout($timeout); + foreach ($this->connections as $connection) { + $connection->setTimeout($timeout); + } + return $this; + } + + /** + * Get timeout. + * @return int<0, max>|float Timeout in seconds + * @deprecated Will be removed in future version, get from Configuration instead + */ + public function getTimeout(): int|float + { + return $this->configuration->getTimeout(); + } + + /** + * Set frame size. + * @param int<1, max> $frameSize Frame size in bytes + * @return self + * @throws InvalidArgumentException If invalid frameSize provided + * @deprecated Will be removed in future version, set on Configuration instead + */ + public function setFrameSize(int $frameSize): self + { + $this->configuration->setFrameSize($frameSize); + foreach ($this->connections as $connection) { + $connection->setFrameSize($frameSize); + } + return $this; + } + + /** + * Get frame size. + * @return int Frame size in bytes + * @deprecated Will be removed in future version, get from Configuration instead + */ + public function getFrameSize(): int + { + return $this->configuration->getFrameSize(); + } + + /** + * Get socket port number. + * @return int port + */ + public function getPort(): int + { + return $this->port; + } + + /** + * Get connection scheme. + * @return string scheme + */ + public function getScheme(): string + { + return $this->scheme; + } + + /** + * Get connection scheme. + * @return bool SSL mode + */ + public function isSsl(): bool + { + return $this->scheme === 'ssl'; + } + + /** + * Number of currently connected clients. + * @return int Connection count + */ + public function getConnectionCount(): int + { + return count($this->connections); + } + + /** + * Get currently connected clients. + * @return array Connections + */ + public function getConnections(): array + { + return $this->connections; + } + + /** + * Get currently readable clients. + * @return array Connections + */ + public function getReadableConnections(): array + { + return array_filter($this->connections, function (Connection $connection) { + return $connection->isReadable(); + }); + } + + /** + * Get currently writable clients. + * @return array Connections + */ + public function getWritableConnections(): array + { + return array_filter($this->connections, function (Connection $connection) { + return $connection->isWritable(); + }); + } + + /** + * Set stream context. + * @param Context|array $context Context or options as array + * @see https://www.php.net/manual/en/context.php + * @return self + * @deprecated Will be removed in future version, set on Configuration instead + */ + public function setContext(Context|array $context): self + { + if ($context instanceof Context) { + $this->configuration->setContext($context); + } else { + $this->configuration->getContext()->setOptions($context); + trigger_error('Calling Server.setContext with array is deprecated, use Context class.', E_USER_DEPRECATED); + } + return $this; + } + + /** + * Get current stream context. + * @return Context + * @deprecated Will be removed in future version, get from Configuration instead + */ + public function getContext(): Context + { + return $this->configuration->getContext(); + } + + /** + * Add a middleware. + * @param MiddlewareInterface $middleware + * @return self + */ + public function addMiddleware(MiddlewareInterface $middleware): self + { + $this->middlewares[] = $middleware; + foreach ($this->connections as $connection) { + $connection->addMiddleware($middleware); + } + return $this; + } + + /** + * Set maximum number of connections allowed, null means unlimited. + * @param int<1, max>|null $maxConnections + * @return self + * @throws InvalidArgumentException If number provided + * @deprecated Will be removed in future version, set on Configuration instead + */ + public function setMaxConnections(int|null $maxConnections): self + { + $this->configuration->setMaxConnections($maxConnections); + return $this; + } + + + /* ---------- Messaging operations ----------------------------------------------------------------------------- */ + + /** + * Send message (broadcast to all connected clients). + * @template T of Message + * @param T $message + * @return T + */ + public function send(Message $message): Message + { + foreach ($this->connections as $connection) { + if ($connection->isWritable()) { + $connection->send($message); + } + } + return $message; + } + + + /* ---------- Listener operations ------------------------------------------------------------------------------ */ + + /** + * Start server listener. + * @throws Throwable On low level error + */ + public function start(int|float|null $timeout = null): void + { + // Create socket server + if (empty($this->server)) { + $this->createSocketServer(); + } + + // Check if running + if ($this->running) { + $this->configuration->getLogger()->warning("[{scope}] Server is already running", [ + 'scope' => self::SCOPE, + 'server' => $this->identity, + ]); + return; + } + $this->running = true; + $this->configuration->getLogger()->info("[{scope}] Server is running", [ + 'scope' => self::SCOPE, + 'server' => $this->identity, + ]); + + // Run handler + while ($this->running) { + try { + // Clear closed connections + $this->detachUnconnected(); + + if (!$this->server) { + $this->stop(); + return; + } + + // Run attached handlers on selected streams + $this->runner->handle($timeout ?? $this->configuration->getTimeout()); + + foreach ($this->connections as $connection) { + $connection->tick(); + } + $this->dispatch('tick', [$this]); + } catch (ExceptionInterface $e) { + // Low-level error + $this->configuration->getLogger()->error("[{scope}] {message}", [ + 'scope' => self::SCOPE, + 'server' => $this->identity, + 'exception' => $e, + 'message' => $e->getMessage(), + ]); + $this->dispatch('error', [$this, null, $e]); + } catch (Throwable $e) { + // Crash it + $this->configuration->getLogger()->error("[{scope}] {message}", [ + 'scope' => self::SCOPE, + 'server' => $this->identity, + 'exception' => $e, + 'message' => $e->getMessage(), + ]); + $this->disconnect(); + throw $e; + } + gc_collect_cycles(); // Collect garbage + } + } + + /** + * Stop server listener (resumable). + */ + public function stop(): void + { + $this->running = false; + $this->configuration->getLogger()->info("[{scope}] Server is stopped", [ + 'scope' => self::SCOPE, + 'server' => $this->identity, + ]); + } + + /** + * If server is running (accepting connections and messages). + * @return bool + */ + public function isRunning(): bool + { + return $this->running; + } + + + /* ---------- Connection management ---------------------------------------------------------------------------- */ + + /** + * Orderly shutdown of server. + * @param int $closeStatus Default is 1001 "Going away" + */ + public function shutdown(int $closeStatus = 1001): void + { + $this->configuration->getLogger()->info("[{scope}] Shutting down", [ + 'scope' => self::SCOPE, + 'server' => $this->identity, + ]); + if ($this->getConnectionCount() == 0) { + $this->disconnect(); + return; + } + // Store and reset settings, lock new connections, reset listeners + $this->allowConnections = false; + $listeners = $this->listeners; + $this->listeners = []; + // Track disconnects + $this->onDisconnect(function () use ($listeners) { + if ($this->getConnectionCount() > 0) { + return; + } + $this->disconnect(); + // Restore settings + $this->listeners = $listeners; + }); + // Close all current connections, listen to acks + $this->close($closeStatus); + $this->start(); + } + + /** + * Disconnect all connections and stop server. + */ + public function disconnect(): void + { + $this->running = false; + foreach ($this->connections as $connection) { + $connection->disconnect(); + $this->runner->detach($connection->getIdentity()); + $this->dispatch('disconnect', [$this, $connection]); + } + $this->connections = []; + if ($this->server) { + $this->server->close(); + $this->runner->detach($this->identity); + } + $this->server = null; + $this->configuration->getLogger()->info("[{scope}] Server disconnected", [ + 'scope' => self::SCOPE, + 'server' => $this->identity, + ]); + } + + public function getStream(): SocketServer + { + return $this->server ?? $this->createSocketServer(); + } + + + /* ---------- Internal helper methods -------------------------------------------------------------------------- */ + + // Create socket server + protected function createSocketServer(): SocketServer + { + try { + $uri = new Uri("{$this->scheme}://0.0.0.0:{$this->port}"); + $this->server = $server = $this->streamFactory->createSocketServer( + $uri, + $this->configuration->getContext() + ); + $this->runner->attach($this, function (Runner $runner, Server $server) { + $this->acceptSocket($server->getStream()); + }, $this->getIdentity()); + $this->allowConnections = true; + $this->configuration->getLogger()->info("[{scope}] Starting server on {uri}", [ + 'scope' => self::SCOPE, + 'server' => $this->identity, + 'uri' => $uri, + ]); + return $server; + } catch (Throwable $e) { + $error = "Server failed to start: {$e->getMessage()}"; + throw new ServerException($error); + } + } + + /** + * Accept connection on socket server + * @throws ConnectionFailureException + */ + protected function acceptSocket(SocketServer $socket): void + { + $maxConnections = $this->configuration->getMaxConnections(); + if (!is_null($maxConnections) && $this->getConnectionCount() >= $maxConnections) { + $this->configuration->getLogger()->warning("[{scope}] Denied connection, reached max {maxConnections}", [ + 'scope' => self::SCOPE, + 'server' => $this->identity, + 'connections' => $this->getConnectionCount(), + 'maxConnections' => $maxConnections, + ]); + return; + } + if (!$this->allowConnections) { + $this->configuration->getLogger()->warning("[{scope}] Denied connection, shutting down", [ + 'scope' => self::SCOPE, + 'server' => $this->identity, + ]); + return; + } + try { + /** @var SocketStream $stream */ + $stream = $socket->accept(); + $connection = new Connection( + $stream, + false, + true, + $this->isSsl(), + $this->httpFactory, + clone $this->configuration + ); + $this->runner->attach($connection, function (Runner $runner, Connection $connection) { + $key = $connection->getIdentity(); + + try { + $message = $connection->pullMessage(); + $this->dispatch($message->getOpcode(), [$this, $connection, $message]); + } catch (MessageLevelInterface $e) { + // Error, but keep connection open + $this->configuration->getLogger()->error("[{scope}] {message}", [ + 'scope' => self::SCOPE, + 'server' => $this->identity, + 'connection' => $connection->getIdentity(), + 'exception' => $e, + 'message' => $e->getMessage(), + ]); + $this->dispatch('error', [$this, $connection, $e]); + } catch (ConnectionLevelInterface $e) { + // Error, disconnect connection + $this->runner->detach($key); + unset($this->connections[$key]); + $connection->disconnect(); + $this->configuration->getLogger()->error("[{scope}] {message}", [ + 'scope' => self::SCOPE, + 'server' => $this->identity, + 'exception' => $e, + 'message' => $e->getMessage(), + ]); + $this->dispatch('error', [$this, $connection, $e]); + } catch (CloseException $e) { + // Should close + $connection->close($e->getCloseStatus(), $e->getMessage()); + $this->configuration->getLogger()->error("[{scope}] {message}", [ + 'scope' => self::SCOPE, + 'server' => $this->identity, + 'connection' => $connection->getIdentity(), + 'exception' => $e, + 'message' => $e->getMessage(), + ]); + $this->dispatch('error', [$this, $connection, $e]); + } + }, $connection->getIdentity()); + } catch (StreamException $e) { + throw new ConnectionFailureException("Server failed to accept: {$e->getMessage()}"); + } + try { + foreach ($this->middlewares as $middleware) { + $connection->addMiddleware($middleware); + } + /** @throws StreamException */ + $request = $this->performHandshake($connection); + $this->connections[$connection->getIdentity()] = $connection; + $this->configuration->getLogger()->info("[{scope}] Accepted connection from {connection}", [ + 'scope' => self::SCOPE, + 'server' => $this->identity, + 'connection' => $connection->getIdentity(), + ]); + + $this->dispatch('handshake', [ + $this, + $connection, + $connection->getHandshakeRequest(), + $connection->getHandshakeResponse(), + ]); + $this->dispatch('connect', [$this, $connection, $request]); + } catch (ExceptionInterface | StreamException $e) { + $this->runner->detach($connection->getIdentity()); + $connection->disconnect(); + throw new ConnectionFailureException("Server failed to accept: {$e->getMessage()}"); + } + } + + // Detach connections no longer available + protected function detachUnconnected(): void + { + foreach ($this->connections as $key => $connection) { + if (!$connection->isConnected()) { + $this->runner->detach($key); + unset($this->connections[$key]); + $this->configuration->getLogger()->info("[{scope}] Disconnected {connection}", [ + 'scope' => self::SCOPE, + 'server' => $this->identity, + 'connection' => $connection->getIdentity(), + ]); + $this->dispatch('disconnect', [$this, $connection]); + } + } + } + + // Perform upgrade handshake on new connections. + protected function performHandshake(Connection $connection): ServerRequestInterface + { + $response = $this->httpFactory->createResponse(101, 'Switching Protocols'); + $exception = null; + + // Read handshake request + /** @var ServerRequestInterface */ + $request = $connection->pullHttp(); + + // Verify handshake request + try { + if ($request->getMethod() != 'GET') { + throw new HandshakeException( + "Handshake request with invalid method: '{$request->getMethod()}'", + $response->withStatus(405) + ); + } + $connectionHeader = trim($request->getHeaderLine('Connection')); + if (!str_contains(strtolower($connectionHeader), 'upgrade')) { + throw new HandshakeException( + "Handshake request with invalid Connection header: '{$connectionHeader}'", + $response->withStatus(426) + ); + } + $upgradeHeader = trim($request->getHeaderLine('Upgrade')); + if (strtolower($upgradeHeader) != 'websocket') { + throw new HandshakeException( + "Handshake request with invalid Upgrade header: '{$upgradeHeader}'", + $response->withStatus(426) + ); + } + $versionHeader = trim($request->getHeaderLine('Sec-WebSocket-Version')); + if ($versionHeader != '13') { + throw new HandshakeException( + "Handshake request with invalid Sec-WebSocket-Version header: '{$versionHeader}'", + $response->withStatus(426)->withHeader('Sec-WebSocket-Version', '13') + ); + } + $keyHeader = trim($request->getHeaderLine('Sec-WebSocket-Key')); + if (empty($keyHeader)) { + throw new HandshakeException( + "Handshake request with invalid Sec-WebSocket-Key header: '{$keyHeader}'", + $response->withStatus(426) + ); + } + if (strlen(base64_decode($keyHeader)) != 16) { + throw new HandshakeException( + "Handshake request with invalid Sec-WebSocket-Key header: '{$keyHeader}'", + $response->withStatus(426) + ); + } + + $responseKey = base64_encode(pack('H*', sha1($keyHeader . Constant::GUID))); + $response = $response + ->withHeader('Upgrade', 'websocket') + ->withHeader('Connection', 'Upgrade') + ->withHeader('Sec-WebSocket-Accept', $responseKey); + } catch (HandshakeException $e) { + $this->configuration->getLogger()->warning("[{scope}] {message}", [ + 'scope' => self::SCOPE, + 'server' => $this->identity, + 'connection' => $connection->getIdentity(), + 'exception' => $e, + 'message' => $e->getMessage(), + ]); + $response = $e->getResponse(); + $exception = $e; + } + + // Respond to handshake + /** @var ResponseInterface */ + $response = $connection->pushHttp($response); + if ($response->getStatusCode() != 101) { + $exception = new HandshakeException("Invalid status code {$response->getStatusCode()}", $response); + } + + if ($exception) { + throw $exception; + } + + $this->configuration->getLogger()->debug("[{scope}] Handshake on {path}", [ + 'scope' => self::SCOPE, + 'server' => $this->identity, + 'connection' => $connection->getIdentity(), + 'path' => $request->getUri()->getPath(), + ]); + + $connection->setHandshakeRequest($request); + $connection->setHandshakeResponse($response); + + return $request; + } +} diff --git a/vendor/phrity/websocket/src/Trait/ConfigurationTrait.php b/vendor/phrity/websocket/src/Trait/ConfigurationTrait.php new file mode 100644 index 000000000..fd66db253 --- /dev/null +++ b/vendor/phrity/websocket/src/Trait/ConfigurationTrait.php @@ -0,0 +1,35 @@ +configuration = $configuration ?? new Configuration(); + return $this; + } + + public function getConfiguration(): Configuration + { + return $this->configuration; + } + + public function setConfiguration(Configuration $configuration): self + { + $this->configuration = $configuration; + return $this; + } +} diff --git a/vendor/phrity/websocket/src/Trait/ListenerTrait.php b/vendor/phrity/websocket/src/Trait/ListenerTrait.php new file mode 100644 index 000000000..16eeb7422 --- /dev/null +++ b/vendor/phrity/websocket/src/Trait/ListenerTrait.php @@ -0,0 +1,126 @@ + $listeners */ + private array $listeners = []; + + /** + * @param Closure(T, Connection, RequestInterface|ResponseInterface): void $closure + * @deprecated Will be removed in v4 + */ + public function onConnect(Closure $closure): self + { + $msg = 'onConnect() is deprecated and will be removed in v4. Use onHandshake() instead.'; + trigger_error($msg, E_USER_DEPRECATED); + $this->listeners['connect'] = $closure; + return $this; + } + + /** @param Closure(T, Connection): void $closure */ + public function onDisconnect(Closure $closure): self + { + $this->listeners['disconnect'] = $closure; + return $this; + } + + /** @param Closure(T, Connection, RequestInterface, ResponseInterface): void $closure */ + public function onHandshake(Closure $closure): self + { + $this->listeners['handshake'] = $closure; + return $this; + } + + /** @param Closure(T, Connection, Text): void $closure */ + public function onText(Closure $closure): self + { + $this->listeners['text'] = $closure; + return $this; + } + + /** @param Closure(T, Connection, Binary): void $closure */ + public function onBinary(Closure $closure): self + { + $this->listeners['binary'] = $closure; + return $this; + } + + /** @param Closure(T, Connection, Ping): void $closure */ + public function onPing(Closure $closure): self + { + $this->listeners['ping'] = $closure; + return $this; + } + + /** @param Closure(T, Connection, Pong): void $closure */ + public function onPong(Closure $closure): self + { + $this->listeners['pong'] = $closure; + return $this; + } + + /** @param Closure(T, Connection, Close): void $closure */ + public function onClose(Closure $closure): self + { + $this->listeners['close'] = $closure; + return $this; + } + + /** @param Closure(T, Connection|null, ExceptionInterface): void $closure */ + public function onError(Closure $closure): self + { + $this->listeners['error'] = $closure; + return $this; + } + + /** @param Closure(T): void $closure */ + public function onTick(Closure $closure): self + { + $this->listeners['tick'] = $closure; + return $this; + } + + /** + * @param array{ + * 0: T, + * 1?: Connection|null, + * 2?: Message|Text|Binary|Close|Ping|Pong|RequestInterface|ResponseInterface|ExceptionInterface|null, + * 3?: ResponseInterface|null + * } $args + */ + private function dispatch(string $type, array $args): void + { + if (array_key_exists($type, $this->listeners)) { + $closure = $this->listeners[$type]; + call_user_func_array($closure, $args); + } + } +} diff --git a/vendor/phrity/websocket/src/Trait/OpcodeTrait.php b/vendor/phrity/websocket/src/Trait/OpcodeTrait.php new file mode 100644 index 000000000..cba9b7c77 --- /dev/null +++ b/vendor/phrity/websocket/src/Trait/OpcodeTrait.php @@ -0,0 +1,25 @@ + $opcodes */ + private static array $opcodes = [ + 'continuation' => 0, + 'text' => 1, + 'binary' => 2, + 'close' => 8, + 'ping' => 9, + 'pong' => 10, + ]; +} diff --git a/vendor/phrity/websocket/src/Trait/SendMethodsTrait.php b/vendor/phrity/websocket/src/Trait/SendMethodsTrait.php new file mode 100644 index 000000000..810ce6283 --- /dev/null +++ b/vendor/phrity/websocket/src/Trait/SendMethodsTrait.php @@ -0,0 +1,74 @@ +send(new Text($message)); + } + + /** + * Send binary message. + * @param string $message Content as binary string. + * @return Binary instance + */ + public function binary(string $message): Binary + { + return $this->send(new Binary($message)); + } + + /** + * Send ping. + * @param string $message Optional text as string. + * @return Ping instance + */ + public function ping(string $message = ''): Ping + { + return $this->send(new Ping($message)); + } + + /** + * Send unsolicited pong. + * @param string $message Optional text as string. + * @return Pong instance + */ + public function pong(string $message = ''): Pong + { + return $this->send(new Pong($message)); + } + + /** + * 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. + * @return Close instance + */ + public function close(int $status = 1000, string $message = 'ttfn'): Close + { + return $this->send(new Close($status, $message)); + } +} diff --git a/vendor/phrity/websocket/src/Trait/StringableTrait.php b/vendor/phrity/websocket/src/Trait/StringableTrait.php new file mode 100644 index 000000000..6a519721a --- /dev/null +++ b/vendor/phrity/websocket/src/Trait/StringableTrait.php @@ -0,0 +1,25 @@ +