commit eec520f969bd29fa3e9c2b87f5958ac35e7c91b2 Author: Enoch Date: Sun Nov 10 21:16:01 2024 +0800 init diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..c1c3687 --- /dev/null +++ b/composer.json @@ -0,0 +1,21 @@ +{ + "name": "laysense/monitor", + "description": "A package to use Websocket API of ping0.cc to monitor the ping latency of target host [TEST ONLY]", + "type": "library", + "require": { + "textalk/websocket": "dev-master" + }, + "license": "AGPL", + "autoload": { + "psr-4": { + "Laysense\\Monitor\\": "src/" + } + }, + "authors": [ + { + "name": "Enoch", + "email": "enoch@laysese.com" + } + ], + "minimum-stability": "dev" +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..c1729b9 --- /dev/null +++ b/composer.lock @@ -0,0 +1,375 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "7f9863daaab5193cdcf774ddaa946c8d", + "packages": [ + { + "name": "phrity/net-uri", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/sirn-se/phrity-net-uri.git", + "reference": "3f458e0c4d1ddc0e218d7a5b9420127c63925f43" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sirn-se/phrity-net-uri/zipball/3f458e0c4d1ddc0e218d7a5b9420127c63925f43", + "reference": "3f458e0c4d1ddc0e218d7a5b9420127c63925f43", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": "^7.4 | ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0 | ^2.0" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.0", + "phpunit/phpunit": "^9.0 | ^10.0", + "squizlabs/php_codesniffer": "^3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Phrity\\Net\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sören Jensen", + "email": "sirn@sirn.se", + "homepage": "https://phrity.sirn.se" + } + ], + "description": "PSR-7 Uri and PSR-17 UriFactory implementation", + "homepage": "https://phrity.sirn.se/net-uri", + "keywords": [ + "psr-17", + "psr-7", + "uri", + "uri factory" + ], + "support": { + "issues": "https://github.com/sirn-se/phrity-net-uri/issues", + "source": "https://github.com/sirn-se/phrity-net-uri/tree/1.3.0" + }, + "time": "2023-08-21T10:33:06+00:00" + }, + { + "name": "phrity/util-errorhandler", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/sirn-se/phrity-util-errorhandler.git", + "reference": "483228156e06673963902b1cc1e6bd9541ab4d5e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sirn-se/phrity-util-errorhandler/zipball/483228156e06673963902b1cc1e6bd9541ab4d5e", + "reference": "483228156e06673963902b1cc1e6bd9541ab4d5e", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": "^7.4 | ^8.0" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.0", + "phpunit/phpunit": "^9.0 | ^10.0 | ^11.0", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Phrity\\Util\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sören Jensen", + "email": "sirn@sirn.se", + "homepage": "https://phrity.sirn.se" + } + ], + "description": "Inline error handler; catch and resolve errors for code block.", + "homepage": "https://phrity.sirn.se/util-errorhandler", + "keywords": [ + "error", + "warning" + ], + "support": { + "issues": "https://github.com/sirn-se/phrity-util-errorhandler/issues", + "source": "https://github.com/sirn-se/phrity-util-errorhandler/tree/1.1.1" + }, + "time": "2024-09-12T06:49:16+00:00" + }, + { + "name": "psr/http-factory", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "7037f4b0950474e9d1350e8df89b15f1842085f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/7037f4b0950474e9d1350e8df89b15f1842085f6", + "reference": "7037f4b0950474e9d1350e8df89b15f1842085f6", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.0.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "default-branch": true, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2023-09-22T11:16:44+00:00" + }, + { + "name": "psr/http-message", + "version": "1.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/1.1" + }, + "time": "2023-04-04T09:50:52+00:00" + }, + { + "name": "psr/log", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=8.0.0" + }, + "default-branch": true, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "textalk/websocket", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/Textalk/websocket-php.git", + "reference": "34b2f0efa2e1c071b046e2b98848178fddf21552" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Textalk/websocket-php/zipball/34b2f0efa2e1c071b046e2b98848178fddf21552", + "reference": "34b2f0efa2e1c071b046e2b98848178fddf21552", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": "^7.4 | ^8.0", + "phrity/net-uri": "^1.0", + "phrity/util-errorhandler": "^1.0", + "psr/http-message": "^1.0", + "psr/log": "^1.0 | ^2.0 | ^3.0" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.0", + "phpunit/phpunit": "^9.0", + "squizlabs/php_codesniffer": "^3.5" + }, + "default-branch": true, + "type": "library", + "autoload": { + "psr-4": { + "WebSocket\\": "lib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Fredrik Liljegren" + }, + { + "name": "Sören Jensen" + } + ], + "description": "WebSocket client and server", + "support": { + "issues": "https://github.com/Textalk/websocket-php/issues", + "source": "https://github.com/Textalk/websocket-php/tree/master" + }, + "time": "2023-12-16T14:43:30+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": { + "textalk/websocket": 20 + }, + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.6.0" +} diff --git a/src/Monitor.php b/src/Monitor.php new file mode 100644 index 0000000..f4366bd --- /dev/null +++ b/src/Monitor.php @@ -0,0 +1,70 @@ +callWebSocet('{"msg":"getnodelist"}'); + $this->nodeinfo=$nodeinfo; + } + public function ping($host,$count) + { + $ping = $this->callWebSocet('{"msg":"ping","target:'.$host.'","type":"ICMP","count":'.$count.'}'); + foreach($this->nodeinfo as $node){ + $ping[$node->nodeid]['nodeinfo']=$node; + } + $this->ping=$ping; + } + public function getDelay() + { + $alldelay = 0; + foreach($this->ping as $id=>$node){ + if((!isset($node['nodeinfo'])) || (!isset($node['delay'])) || $node['delay']<=10){ + unset($this->ping[$id]); + }else{ + $alldelay += $node['delay']; + } + } + $alldelay = $alldelay/count($this->ping); + return $alldelay; + } + private function callWebSocet(){ + $client = new Client($this->dest); + + // 发送消息 + $client->send($msg); + + // 接收消息 + while (true) { + try { + $receive=$client->receive(); + $receive = json_decode($receive); + if($receive->msg="setnodes"&&isset($receive->nodes)){ + $response = $receive->nodes; + break; + }elseif(isset($receive->delay)){ + $response[$receive->nodeid]['delay'] = $receive->delay/1000; + }else{ + $response[$receive->nodeid]['ip'] = $receive->ip; + $response[$receive->nodeid]['loc'] = $receive->loc; + $response[$receive->nodeid]['asn'] = $receive->asn; + $response[$receive->nodeid]['org'] = $receive->org; + } + } catch (\WebSocket\ConnectionException $e) { + break; + } + + } + + // 关闭连接 + $client->close(); + + return $response; + } + +} \ No newline at end of file diff --git a/vendor/autoload.php b/vendor/autoload.php new file mode 100644 index 0000000..45db919 --- /dev/null +++ b/vendor/autoload.php @@ -0,0 +1,25 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Autoload; + +/** + * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. + * + * $loader = new \Composer\Autoload\ClassLoader(); + * + * // register classes with namespaces + * $loader->add('Symfony\Component', __DIR__.'/component'); + * $loader->add('Symfony', __DIR__.'/framework'); + * + * // activate the autoloader + * $loader->register(); + * + * // to enable searching the include path (eg. for PEAR packages) + * $loader->setUseIncludePath(true); + * + * In this example, if you try to use a class in the Symfony\Component + * namespace or one of its children (Symfony\Component\Console for instance), + * the autoloader will first look for the class under the component/ + * directory, and it will then fallback to the framework/ directory if not + * found before giving up. + * + * This class is loosely based on the Symfony UniversalClassLoader. + * + * @author Fabien Potencier + * @author Jordi Boggiano + * @see https://www.php-fig.org/psr/psr-0/ + * @see https://www.php-fig.org/psr/psr-4/ + */ +class ClassLoader +{ + /** @var \Closure(string):void */ + private static $includeFile; + + /** @var string|null */ + private $vendorDir; + + // PSR-4 + /** + * @var array> + */ + private $prefixLengthsPsr4 = array(); + /** + * @var array> + */ + private $prefixDirsPsr4 = array(); + /** + * @var list + */ + private $fallbackDirsPsr4 = array(); + + // PSR-0 + /** + * List of PSR-0 prefixes + * + * Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2'))) + * + * @var array>> + */ + private $prefixesPsr0 = array(); + /** + * @var list + */ + private $fallbackDirsPsr0 = array(); + + /** @var bool */ + private $useIncludePath = false; + + /** + * @var array + */ + private $classMap = array(); + + /** @var bool */ + private $classMapAuthoritative = false; + + /** + * @var array + */ + private $missingClasses = array(); + + /** @var string|null */ + private $apcuPrefix; + + /** + * @var array + */ + private static $registeredLoaders = array(); + + /** + * @param string|null $vendorDir + */ + public function __construct($vendorDir = null) + { + $this->vendorDir = $vendorDir; + self::initializeIncludeClosure(); + } + + /** + * @return array> + */ + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); + } + + return array(); + } + + /** + * @return array> + */ + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + /** + * @return list + */ + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + /** + * @return list + */ + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + /** + * @return array Array of classname => path + */ + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param array $classMap Class to filename map + * + * @return void + */ + public function addClassMap(array $classMap) + { + if ($this->classMap) { + $this->classMap = array_merge($this->classMap, $classMap); + } else { + $this->classMap = $classMap; + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, either + * appending or prepending to the ones previously set for this prefix. + * + * @param string $prefix The prefix + * @param list|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + * + * @return void + */ + public function add($prefix, $paths, $prepend = false) + { + $paths = (array) $paths; + if (!$prefix) { + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], + $paths + ); + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, either + * appending or prepending to the ones previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param list|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories + * + * @throws \InvalidArgumentException + * + * @return void + */ + public function addPsr4($prefix, $paths, $prepend = false) + { + $paths = (array) $paths; + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + $paths + ); + } + } elseif (!isset($this->prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + $paths + ); + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, + * replacing any others previously set for this prefix. + * + * @param string $prefix The prefix + * @param list|string $paths The PSR-0 base directories + * + * @return void + */ + public function set($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr0 = (array) $paths; + } else { + $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, + * replacing any others previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param list|string $paths The PSR-4 base directories + * + * @throws \InvalidArgumentException + * + * @return void + */ + public function setPsr4($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr4 = (array) $paths; + } else { + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } + } + + /** + * Turns on searching the include path for class files. + * + * @param bool $useIncludePath + * + * @return void + */ + public function setUseIncludePath($useIncludePath) + { + $this->useIncludePath = $useIncludePath; + } + + /** + * Can be used to check if the autoloader uses the include path to check + * for classes. + * + * @return bool + */ + public function getUseIncludePath() + { + return $this->useIncludePath; + } + + /** + * Turns off searching the prefix and fallback directories for classes + * that have not been registered with the class map. + * + * @param bool $classMapAuthoritative + * + * @return void + */ + public function setClassMapAuthoritative($classMapAuthoritative) + { + $this->classMapAuthoritative = $classMapAuthoritative; + } + + /** + * Should class lookup fail if not found in the current class map? + * + * @return bool + */ + public function isClassMapAuthoritative() + { + return $this->classMapAuthoritative; + } + + /** + * APCu prefix to use to cache found/not-found classes, if the extension is enabled. + * + * @param string|null $apcuPrefix + * + * @return void + */ + public function setApcuPrefix($apcuPrefix) + { + $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; + } + + /** + * The APCu prefix in use, or null if APCu caching is not enabled. + * + * @return string|null + */ + public function getApcuPrefix() + { + return $this->apcuPrefix; + } + + /** + * Registers this instance as an autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not + * + * @return void + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + + if (null === $this->vendorDir) { + return; + } + + if ($prepend) { + self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders; + } else { + unset(self::$registeredLoaders[$this->vendorDir]); + self::$registeredLoaders[$this->vendorDir] = $this; + } + } + + /** + * Unregisters this instance as an autoloader. + * + * @return void + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + + if (null !== $this->vendorDir) { + unset(self::$registeredLoaders[$this->vendorDir]); + } + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * @return true|null True if loaded, null otherwise + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + $includeFile = self::$includeFile; + $includeFile($file); + + return true; + } + + return null; + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|false The path if found, false otherwise + */ + public function findFile($class) + { + // class map lookup + if (isset($this->classMap[$class])) { + return $this->classMap[$class]; + } + if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { + return false; + } + if (null !== $this->apcuPrefix) { + $file = apcu_fetch($this->apcuPrefix.$class, $hit); + if ($hit) { + return $file; + } + } + + $file = $this->findFileWithExtension($class, '.php'); + + // Search for Hack files if we are running on HHVM + if (false === $file && defined('HHVM_VERSION')) { + $file = $this->findFileWithExtension($class, '.hh'); + } + + if (null !== $this->apcuPrefix) { + apcu_add($this->apcuPrefix.$class, $file); + } + + if (false === $file) { + // Remember that this class does not exist. + $this->missingClasses[$class] = true; + } + + return $file; + } + + /** + * Returns the currently registered loaders keyed by their corresponding vendor directories. + * + * @return array + */ + public static function getRegisteredLoaders() + { + return self::$registeredLoaders; + } + + /** + * @param string $class + * @param string $ext + * @return string|false + */ + private function findFileWithExtension($class, $ext) + { + // PSR-4 lookup + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; + + $first = $class[0]; + if (isset($this->prefixLengthsPsr4[$first])) { + $subPath = $class; + while (false !== $lastPos = strrpos($subPath, '\\')) { + $subPath = substr($subPath, 0, $lastPos); + $search = $subPath . '\\'; + if (isset($this->prefixDirsPsr4[$search])) { + $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); + foreach ($this->prefixDirsPsr4[$search] as $dir) { + if (file_exists($file = $dir . $pathEnd)) { + return $file; + } + } + } + } + } + + // PSR-4 fallback dirs + foreach ($this->fallbackDirsPsr4 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // PSR-0 lookup + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; + } + + if (isset($this->prefixesPsr0[$first])) { + foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // PSR-0 fallback dirs + foreach ($this->fallbackDirsPsr0 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + // PSR-0 include paths. + if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { + return $file; + } + + return false; + } + + /** + * @return void + */ + private static function initializeIncludeClosure() + { + if (self::$includeFile !== null) { + return; + } + + /** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + * + * @param string $file + * @return void + */ + self::$includeFile = \Closure::bind(static function($file) { + include $file; + }, null, null); + } +} diff --git a/vendor/composer/InstalledVersions.php b/vendor/composer/InstalledVersions.php new file mode 100644 index 0000000..51e734a --- /dev/null +++ b/vendor/composer/InstalledVersions.php @@ -0,0 +1,359 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer; + +use Composer\Autoload\ClassLoader; +use Composer\Semver\VersionParser; + +/** + * This class is copied in every Composer installed project and available to all + * + * See also https://getcomposer.org/doc/07-runtime.md#installed-versions + * + * To require its presence, you can require `composer-runtime-api ^2.0` + * + * @final + */ +class InstalledVersions +{ + /** + * @var mixed[]|null + * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array}|array{}|null + */ + private static $installed; + + /** + * @var bool|null + */ + private static $canGetVendors; + + /** + * @var array[] + * @psalm-var array}> + */ + private static $installedByVendor = array(); + + /** + * Returns a list of all package names which are present, either by being installed, replaced or provided + * + * @return string[] + * @psalm-return list + */ + public static function getInstalledPackages() + { + $packages = array(); + foreach (self::getInstalled() as $installed) { + $packages[] = array_keys($installed['versions']); + } + + if (1 === \count($packages)) { + return $packages[0]; + } + + return array_keys(array_flip(\call_user_func_array('array_merge', $packages))); + } + + /** + * Returns a list of all package names with a specific type e.g. 'library' + * + * @param string $type + * @return string[] + * @psalm-return list + */ + public static function getInstalledPackagesByType($type) + { + $packagesByType = array(); + + foreach (self::getInstalled() as $installed) { + foreach ($installed['versions'] as $name => $package) { + if (isset($package['type']) && $package['type'] === $type) { + $packagesByType[] = $name; + } + } + } + + return $packagesByType; + } + + /** + * Checks whether the given package is installed + * + * This also returns true if the package name is provided or replaced by another package + * + * @param string $packageName + * @param bool $includeDevRequirements + * @return bool + */ + public static function isInstalled($packageName, $includeDevRequirements = true) + { + foreach (self::getInstalled() as $installed) { + if (isset($installed['versions'][$packageName])) { + return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false; + } + } + + return false; + } + + /** + * Checks whether the given package satisfies a version constraint + * + * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call: + * + * Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3') + * + * @param VersionParser $parser Install composer/semver to have access to this class and functionality + * @param string $packageName + * @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package + * @return bool + */ + public static function satisfies(VersionParser $parser, $packageName, $constraint) + { + $constraint = $parser->parseConstraints((string) $constraint); + $provided = $parser->parseConstraints(self::getVersionRanges($packageName)); + + return $provided->matches($constraint); + } + + /** + * Returns a version constraint representing all the range(s) which are installed for a given package + * + * It is easier to use this via isInstalled() with the $constraint argument if you need to check + * whether a given version of a package is installed, and not just whether it exists + * + * @param string $packageName + * @return string Version constraint usable with composer/semver + */ + public static function getVersionRanges($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + $ranges = array(); + if (isset($installed['versions'][$packageName]['pretty_version'])) { + $ranges[] = $installed['versions'][$packageName]['pretty_version']; + } + if (array_key_exists('aliases', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']); + } + if (array_key_exists('replaced', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']); + } + if (array_key_exists('provided', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']); + } + + return implode(' || ', $ranges); + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present + */ + public static function getVersion($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['version'])) { + return null; + } + + return $installed['versions'][$packageName]['version']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present + */ + public static function getPrettyVersion($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['pretty_version'])) { + return null; + } + + return $installed['versions'][$packageName]['pretty_version']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference + */ + public static function getReference($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['reference'])) { + return null; + } + + return $installed['versions'][$packageName]['reference']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path. + */ + public static function getInstallPath($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @return array + * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool} + */ + public static function getRootPackage() + { + $installed = self::getInstalled(); + + return $installed[0]['root']; + } + + /** + * Returns the raw installed.php data for custom implementations + * + * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect. + * @return array[] + * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} + */ + public static function getRawData() + { + @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED); + + if (null === self::$installed) { + // only require the installed.php file if this file is loaded from its dumped location, + // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 + if (substr(__DIR__, -8, 1) !== 'C') { + self::$installed = include __DIR__ . '/installed.php'; + } else { + self::$installed = array(); + } + } + + return self::$installed; + } + + /** + * Returns the raw data of all installed.php which are currently loaded for custom implementations + * + * @return array[] + * @psalm-return list}> + */ + public static function getAllRawData() + { + return self::getInstalled(); + } + + /** + * Lets you reload the static array from another file + * + * This is only useful for complex integrations in which a project needs to use + * this class but then also needs to execute another project's autoloader in process, + * and wants to ensure both projects have access to their version of installed.php. + * + * A typical case would be PHPUnit, where it would need to make sure it reads all + * the data it needs from this class, then call reload() with + * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure + * the project in which it runs can then also use this class safely, without + * interference between PHPUnit's dependencies and the project's dependencies. + * + * @param array[] $data A vendor/composer/installed.php data set + * @return void + * + * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $data + */ + public static function reload($data) + { + self::$installed = $data; + self::$installedByVendor = array(); + } + + /** + * @return array[] + * @psalm-return list}> + */ + private static function getInstalled() + { + if (null === self::$canGetVendors) { + self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders'); + } + + $installed = array(); + + if (self::$canGetVendors) { + foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { + if (isset(self::$installedByVendor[$vendorDir])) { + $installed[] = self::$installedByVendor[$vendorDir]; + } elseif (is_file($vendorDir.'/composer/installed.php')) { + /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ + $required = require $vendorDir.'/composer/installed.php'; + $installed[] = self::$installedByVendor[$vendorDir] = $required; + if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { + self::$installed = $installed[count($installed) - 1]; + } + } + } + } + + if (null === self::$installed) { + // only require the installed.php file if this file is loaded from its dumped location, + // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 + if (substr(__DIR__, -8, 1) !== 'C') { + /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ + $required = require __DIR__ . '/installed.php'; + self::$installed = $required; + } else { + self::$installed = array(); + } + } + + if (self::$installed !== array()) { + $installed[] = self::$installed; + } + + return $installed; + } +} diff --git a/vendor/composer/LICENSE b/vendor/composer/LICENSE new file mode 100644 index 0000000..f27399a --- /dev/null +++ b/vendor/composer/LICENSE @@ -0,0 +1,21 @@ + +Copyright (c) Nils Adermann, Jordi Boggiano + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php new file mode 100644 index 0000000..0fb0a2c --- /dev/null +++ b/vendor/composer/autoload_classmap.php @@ -0,0 +1,10 @@ + $vendorDir . '/composer/InstalledVersions.php', +); diff --git a/vendor/composer/autoload_namespaces.php b/vendor/composer/autoload_namespaces.php new file mode 100644 index 0000000..15a2ff3 --- /dev/null +++ b/vendor/composer/autoload_namespaces.php @@ -0,0 +1,9 @@ + array($vendorDir . '/textalk/websocket/lib'), + 'Psr\\Log\\' => array($vendorDir . '/psr/log/src'), + 'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-factory/src', $vendorDir . '/psr/http-message/src'), + 'Phrity\\Util\\' => array($vendorDir . '/phrity/util-errorhandler/src'), + 'Phrity\\Net\\' => array($vendorDir . '/phrity/net-uri/src'), + 'Laysense\\Monitor\\' => array($baseDir . '/src'), +); diff --git a/vendor/composer/autoload_real.php b/vendor/composer/autoload_real.php new file mode 100644 index 0000000..921d125 --- /dev/null +++ b/vendor/composer/autoload_real.php @@ -0,0 +1,38 @@ +register(true); + + return $loader; + } +} diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php new file mode 100644 index 0000000..3bc399e --- /dev/null +++ b/vendor/composer/autoload_static.php @@ -0,0 +1,68 @@ + + array ( + 'WebSocket\\' => 10, + ), + 'P' => + array ( + 'Psr\\Log\\' => 8, + 'Psr\\Http\\Message\\' => 17, + 'Phrity\\Util\\' => 12, + 'Phrity\\Net\\' => 11, + ), + 'L' => + array ( + 'Laysense\\Monitor\\' => 17, + ), + ); + + public static $prefixDirsPsr4 = array ( + 'WebSocket\\' => + array ( + 0 => __DIR__ . '/..' . '/textalk/websocket/lib', + ), + 'Psr\\Log\\' => + array ( + 0 => __DIR__ . '/..' . '/psr/log/src', + ), + 'Psr\\Http\\Message\\' => + array ( + 0 => __DIR__ . '/..' . '/psr/http-factory/src', + 1 => __DIR__ . '/..' . '/psr/http-message/src', + ), + 'Phrity\\Util\\' => + array ( + 0 => __DIR__ . '/..' . '/phrity/util-errorhandler/src', + ), + 'Phrity\\Net\\' => + array ( + 0 => __DIR__ . '/..' . '/phrity/net-uri/src', + ), + 'Laysense\\Monitor\\' => + array ( + 0 => __DIR__ . '/../..' . '/src', + ), + ); + + public static $classMap = array ( + 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', + ); + + public static function getInitializer(ClassLoader $loader) + { + return \Closure::bind(function () use ($loader) { + $loader->prefixLengthsPsr4 = ComposerStaticInit7f9863daaab5193cdcf774ddaa946c8d::$prefixLengthsPsr4; + $loader->prefixDirsPsr4 = ComposerStaticInit7f9863daaab5193cdcf774ddaa946c8d::$prefixDirsPsr4; + $loader->classMap = ComposerStaticInit7f9863daaab5193cdcf774ddaa946c8d::$classMap; + + }, null, ClassLoader::class); + } +} diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json new file mode 100644 index 0000000..e21dab3 --- /dev/null +++ b/vendor/composer/installed.json @@ -0,0 +1,378 @@ +{ + "packages": [ + { + "name": "phrity/net-uri", + "version": "1.3.0", + "version_normalized": "1.3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sirn-se/phrity-net-uri.git", + "reference": "3f458e0c4d1ddc0e218d7a5b9420127c63925f43" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sirn-se/phrity-net-uri/zipball/3f458e0c4d1ddc0e218d7a5b9420127c63925f43", + "reference": "3f458e0c4d1ddc0e218d7a5b9420127c63925f43", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": "^7.4 | ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0 | ^2.0" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.0", + "phpunit/phpunit": "^9.0 | ^10.0", + "squizlabs/php_codesniffer": "^3.0" + }, + "time": "2023-08-21T10:33:06+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Phrity\\Net\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sören Jensen", + "email": "sirn@sirn.se", + "homepage": "https://phrity.sirn.se" + } + ], + "description": "PSR-7 Uri and PSR-17 UriFactory implementation", + "homepage": "https://phrity.sirn.se/net-uri", + "keywords": [ + "psr-17", + "psr-7", + "uri", + "uri factory" + ], + "support": { + "issues": "https://github.com/sirn-se/phrity-net-uri/issues", + "source": "https://github.com/sirn-se/phrity-net-uri/tree/1.3.0" + }, + "install-path": "../phrity/net-uri" + }, + { + "name": "phrity/util-errorhandler", + "version": "1.1.1", + "version_normalized": "1.1.1.0", + "source": { + "type": "git", + "url": "https://github.com/sirn-se/phrity-util-errorhandler.git", + "reference": "483228156e06673963902b1cc1e6bd9541ab4d5e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sirn-se/phrity-util-errorhandler/zipball/483228156e06673963902b1cc1e6bd9541ab4d5e", + "reference": "483228156e06673963902b1cc1e6bd9541ab4d5e", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": "^7.4 | ^8.0" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.0", + "phpunit/phpunit": "^9.0 | ^10.0 | ^11.0", + "squizlabs/php_codesniffer": "^3.5" + }, + "time": "2024-09-12T06:49:16+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Phrity\\Util\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sören Jensen", + "email": "sirn@sirn.se", + "homepage": "https://phrity.sirn.se" + } + ], + "description": "Inline error handler; catch and resolve errors for code block.", + "homepage": "https://phrity.sirn.se/util-errorhandler", + "keywords": [ + "error", + "warning" + ], + "support": { + "issues": "https://github.com/sirn-se/phrity-util-errorhandler/issues", + "source": "https://github.com/sirn-se/phrity-util-errorhandler/tree/1.1.1" + }, + "install-path": "../phrity/util-errorhandler" + }, + { + "name": "psr/http-factory", + "version": "dev-master", + "version_normalized": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "7037f4b0950474e9d1350e8df89b15f1842085f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/7037f4b0950474e9d1350e8df89b15f1842085f6", + "reference": "7037f4b0950474e9d1350e8df89b15f1842085f6", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.0.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "time": "2023-09-22T11:16:44+00:00", + "default-branch": true, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "install-path": "../psr/http-factory" + }, + { + "name": "psr/http-message", + "version": "1.1", + "version_normalized": "1.1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "time": "2023-04-04T09:50:52+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/1.1" + }, + "install-path": "../psr/http-message" + }, + { + "name": "psr/log", + "version": "dev-master", + "version_normalized": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=8.0.0" + }, + "time": "2024-09-11T13:17:53+00:00", + "default-branch": true, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "install-path": "../psr/log" + }, + { + "name": "textalk/websocket", + "version": "dev-master", + "version_normalized": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/Textalk/websocket-php.git", + "reference": "34b2f0efa2e1c071b046e2b98848178fddf21552" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Textalk/websocket-php/zipball/34b2f0efa2e1c071b046e2b98848178fddf21552", + "reference": "34b2f0efa2e1c071b046e2b98848178fddf21552", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": "^7.4 | ^8.0", + "phrity/net-uri": "^1.0", + "phrity/util-errorhandler": "^1.0", + "psr/http-message": "^1.0", + "psr/log": "^1.0 | ^2.0 | ^3.0" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.0", + "phpunit/phpunit": "^9.0", + "squizlabs/php_codesniffer": "^3.5" + }, + "time": "2023-12-16T14:43:30+00:00", + "default-branch": true, + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "WebSocket\\": "lib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Fredrik Liljegren" + }, + { + "name": "Sören Jensen" + } + ], + "description": "WebSocket client and server", + "support": { + "issues": "https://github.com/Textalk/websocket-php/issues", + "source": "https://github.com/Textalk/websocket-php/tree/master" + }, + "install-path": "../textalk/websocket" + } + ], + "dev": true, + "dev-package-names": [] +} diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php new file mode 100644 index 0000000..977ee16 --- /dev/null +++ b/vendor/composer/installed.php @@ -0,0 +1,83 @@ + array( + 'name' => 'laysense/monitor', + 'pretty_version' => '1.0.0+no-version-set', + 'version' => '1.0.0.0', + 'reference' => null, + 'type' => 'library', + 'install_path' => __DIR__ . '/../../', + 'aliases' => array(), + 'dev' => true, + ), + 'versions' => array( + 'laysense/monitor' => array( + 'pretty_version' => '1.0.0+no-version-set', + 'version' => '1.0.0.0', + 'reference' => null, + 'type' => 'library', + 'install_path' => __DIR__ . '/../../', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'phrity/net-uri' => array( + 'pretty_version' => '1.3.0', + 'version' => '1.3.0.0', + 'reference' => '3f458e0c4d1ddc0e218d7a5b9420127c63925f43', + 'type' => 'library', + 'install_path' => __DIR__ . '/../phrity/net-uri', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'phrity/util-errorhandler' => array( + 'pretty_version' => '1.1.1', + 'version' => '1.1.1.0', + 'reference' => '483228156e06673963902b1cc1e6bd9541ab4d5e', + 'type' => 'library', + 'install_path' => __DIR__ . '/../phrity/util-errorhandler', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'psr/http-factory' => array( + 'pretty_version' => 'dev-master', + 'version' => 'dev-master', + 'reference' => '7037f4b0950474e9d1350e8df89b15f1842085f6', + 'type' => 'library', + 'install_path' => __DIR__ . '/../psr/http-factory', + 'aliases' => array( + 0 => '1.0.x-dev', + ), + 'dev_requirement' => false, + ), + 'psr/http-message' => array( + 'pretty_version' => '1.1', + 'version' => '1.1.0.0', + 'reference' => 'cb6ce4845ce34a8ad9e68117c10ee90a29919eba', + 'type' => 'library', + 'install_path' => __DIR__ . '/../psr/http-message', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'psr/log' => array( + 'pretty_version' => 'dev-master', + 'version' => 'dev-master', + 'reference' => 'f16e1d5863e37f8d8c2a01719f5b34baa2b714d3', + 'type' => 'library', + 'install_path' => __DIR__ . '/../psr/log', + 'aliases' => array( + 0 => '3.x-dev', + ), + 'dev_requirement' => false, + ), + 'textalk/websocket' => array( + 'pretty_version' => 'dev-master', + 'version' => 'dev-master', + 'reference' => '34b2f0efa2e1c071b046e2b98848178fddf21552', + 'type' => 'library', + 'install_path' => __DIR__ . '/../textalk/websocket', + 'aliases' => array( + 0 => '9999999-dev', + ), + 'dev_requirement' => false, + ), + ), +); diff --git a/vendor/composer/platform_check.php b/vendor/composer/platform_check.php new file mode 100644 index 0000000..adfb472 --- /dev/null +++ b/vendor/composer/platform_check.php @@ -0,0 +1,26 @@ += 80000)) { + $issues[] = 'Your Composer dependencies require a PHP version ">= 8.0.0". You are running ' . PHP_VERSION . '.'; +} + +if ($issues) { + if (!headers_sent()) { + header('HTTP/1.1 500 Internal Server Error'); + } + if (!ini_get('display_errors')) { + if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { + fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL); + } elseif (!headers_sent()) { + echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL; + } + } + trigger_error( + 'Composer detected issues in your platform: ' . implode(' ', $issues), + E_USER_ERROR + ); +} diff --git a/vendor/phrity/net-uri/composer.json b/vendor/phrity/net-uri/composer.json new file mode 100644 index 0000000..8f87a3a --- /dev/null +++ b/vendor/phrity/net-uri/composer.json @@ -0,0 +1,30 @@ +{ + "name": "phrity/net-uri", + "type": "library", + "description": "PSR-7 Uri and PSR-17 UriFactory implementation", + "homepage": "https://phrity.sirn.se/net-uri", + "keywords": ["uri", "uri factory", "PSR-7", "PSR-17"], + "license": "MIT", + "authors": [ + { + "name": "Sören Jensen", + "email": "sirn@sirn.se", + "homepage": "https://phrity.sirn.se" + } + ], + "autoload": { + "psr-4": { + "Phrity\\Net\\": "src/" + } + }, + "require": { + "php": "^7.4 | ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0 | ^2.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0 | ^10.0", + "php-coveralls/php-coveralls": "^2.0", + "squizlabs/php_codesniffer": "^3.0" + } +} diff --git a/vendor/phrity/net-uri/src/Uri.php b/vendor/phrity/net-uri/src/Uri.php new file mode 100644 index 0000000..4e0ba65 --- /dev/null +++ b/vendor/phrity/net-uri/src/Uri.php @@ -0,0 +1,486 @@ + Net > Uri + * @see https://www.rfc-editor.org/rfc/rfc3986 + * @see https://www.php-fig.org/psr/psr-7/#35-psrhttpmessageuriinterface + */ + +namespace Phrity\Net; + +use InvalidArgumentException; +use Psr\Http\Message\UriInterface; + +/** + * Net\Uri class. + */ +class Uri implements UriInterface +{ + public const REQUIRE_PORT = 1; // Always include port, explicit or default + public const ABSOLUTE_PATH = 2; // Enforce absolute path + public const NORMALIZE_PATH = 4; // Normalize path + public const IDNA = 8; // IDNA-convert host + + private const RE_MAIN = '!^(?P(?P[^:/?#]+):)?(?P//(?P[^/?#]*))?' + . '(?P[^?#]*)(?P\?(?P[^#]*))?(?P#(?P.*))?$!'; + private const RE_AUTH = '!^(?P(?P[^:/?#]+)(?P:(?P[^:/?#]+))?@)?' + . '(?P[^:/?#]*|\[[^/?#]*\])(?P:(?P[0-9]*))?$!'; + + private static $port_defaults = [ + 'acap' => 674, + 'afp' => 548, + 'dict' => 2628, + 'dns' => 53, + 'ftp' => 21, + 'git' => 9418, + 'gopher' => 70, + 'http' => 80, + 'https' => 443, + 'imap' => 143, + 'ipp' => 631, + 'ipps' => 631, + 'irc' => 194, + 'ircs' => 6697, + 'ldap' => 389, + 'ldaps' => 636, + 'mms' => 1755, + 'msrp' => 2855, + 'mtqp' => 1038, + 'nfs' => 111, + 'nntp' => 119, + 'nntps' => 563, + 'pop' => 110, + 'prospero' => 1525, + 'redis' => 6379, + 'rsync' => 873, + 'rtsp' => 554, + 'rtsps' => 322, + 'rtspu' => 5005, + 'sftp' => 22, + 'smb' => 445, + 'snmp' => 161, + 'ssh' => 22, + 'svn' => 3690, + 'telnet' => 23, + 'ventrilo' => 3784, + 'vnc' => 5900, + 'wais' => 210, + 'ws' => 80, + 'wss' => 443, + ]; + + private $scheme; + private $authority; + private $host; + private $port; + private $user; + private $pass; + private $path; + private $query; + private $fragment; + + /** + * Create new URI instance using a string + * @param string $uri_string URI as string + * @throws \InvalidArgumentException If the given URI cannot be parsed + */ + public function __construct(string $uri_string = '', int $flags = 0) + { + $this->parse($uri_string); + } + + + // ---------- PSR-7 getters --------------------------------------------------------------------------------------- + + /** + * Retrieve the scheme component of the URI. + * @return string The URI scheme + */ + public function getScheme(int $flags = 0): string + { + return $this->getComponent('scheme') ?? ''; + } + + /** + * Retrieve the authority component of the URI. + * @return string The URI authority, in "[user-info@]host[:port]" format + */ + public function getAuthority(int $flags = 0): string + { + $host = $this->formatComponent($this->getHost($flags)); + if ($this->isEmpty($host)) { + return ''; + } + $userinfo = $this->formatComponent($this->getUserInfo(), '', '@'); + $port = $this->formatComponent($this->getPort($flags), ':'); + return "{$userinfo}{$host}{$port}"; + } + + /** + * Retrieve the user information component of the URI. + * @return string The URI user information, in "username[:password]" format + */ + public function getUserInfo(int $flags = 0): string + { + $user = $this->formatComponent($this->getComponent('user')); + $pass = $this->formatComponent($this->getComponent('pass'), ':'); + return $this->isEmpty($user) ? '' : "{$user}{$pass}"; + } + + /** + * Retrieve the host component of the URI. + * @return string The URI host + */ + public function getHost(int $flags = 0): string + { + $host = $this->getComponent('host') ?? ''; + if ($flags & self::IDNA) { + $host = $this->idna($host); + } + return $host; + } + + /** + * Retrieve the port component of the URI. + * @return null|int The URI port + */ + public function getPort(int $flags = 0): ?int + { + $port = $this->getComponent('port'); + $scheme = $this->getComponent('scheme'); + $default = isset(self::$port_defaults[$scheme]) ? self::$port_defaults[$scheme] : null; + if ($flags & self::REQUIRE_PORT) { + return !$this->isEmpty($port) ? $port : $default; + } + return $this->isEmpty($port) || $port === $default ? null : $port; + } + + /** + * Retrieve the path component of the URI. + * @return string The URI path + */ + public function getPath(int $flags = 0): string + { + $path = $this->getComponent('path') ?? ''; + if ($flags & self::NORMALIZE_PATH) { + $path = $this->normalizePath($path); + } + if ($flags & self::ABSOLUTE_PATH && substr($path, 0, 1) !== '/') { + $path = "/{$path}"; + } + return $path; + } + + /** + * Retrieve the query string of the URI. + * @return string The URI query string + */ + public function getQuery(int $flags = 0): string + { + return $this->getComponent('query') ?? ''; + } + + /** + * Retrieve the fragment component of the URI. + * @return string The URI fragment + */ + public function getFragment(int $flags = 0): string + { + return $this->getComponent('fragment') ?? ''; + } + + + // ---------- PSR-7 setters --------------------------------------------------------------------------------------- + + /** + * Return an instance with the specified scheme. + * @param string $scheme The scheme to use with the new instance + * @return static A new instance with the specified scheme + * @throws \InvalidArgumentException for invalid schemes + * @throws \InvalidArgumentException for unsupported schemes + */ + public function withScheme($scheme, int $flags = 0): UriInterface + { + $clone = clone $this; + if ($flags & self::REQUIRE_PORT) { + $clone->setComponent('port', $this->getPort(self::REQUIRE_PORT)); + $default = isset(self::$port_defaults[$scheme]) ? self::$port_defaults[$scheme] : null; + } + $clone->setComponent('scheme', $scheme); + return $clone; + } + + /** + * Return an instance with the specified user information. + * @param string $user The user name to use for authority + * @param null|string $password The password associated with $user + * @return static A new instance with the specified user information + */ + public function withUserInfo($user, $password = null, int $flags = 0): UriInterface + { + $clone = clone $this; + $clone->setComponent('user', $user); + $clone->setComponent('pass', $password); + return $clone; + } + + /** + * Return an instance with the specified host. + * @param string $host The hostname to use with the new instance + * @return static A new instance with the specified host + * @throws \InvalidArgumentException for invalid hostnames + */ + public function withHost($host, int $flags = 0): UriInterface + { + $clone = clone $this; + if ($flags & self::IDNA) { + $host = $this->idna($host); + } + $clone->setComponent('host', $host); + return $clone; + } + + /** + * Return an instance with the specified port. + * @param null|int $port The port to use with the new instance + * @return static A new instance with the specified port + * @throws \InvalidArgumentException for invalid ports + */ + public function withPort($port, int $flags = 0): UriInterface + { + $clone = clone $this; + $clone->setComponent('port', $port); + return $clone; + } + + /** + * Return an instance with the specified path. + * @param string $path The path to use with the new instance + * @return static A new instance with the specified path + * @throws \InvalidArgumentException for invalid paths + */ + public function withPath($path, int $flags = 0): UriInterface + { + $clone = clone $this; + if ($flags & self::NORMALIZE_PATH) { + $path = $this->normalizePath($path); + } + if ($flags & self::ABSOLUTE_PATH && substr($path, 0, 1) !== '/') { + $path = "/{$path}"; + } + $clone->setComponent('path', $path); + return $clone; + } + + /** + * Return an instance with the specified query string. + * @param string $query The query string to use with the new instance + * @return static A new instance with the specified query string + * @throws \InvalidArgumentException for invalid query strings + */ + public function withQuery($query, int $flags = 0): UriInterface + { + $clone = clone $this; + $clone->setComponent('query', $query); + return $clone; + } + + /** + * Return an instance with the specified URI fragment. + * @param string $fragment The fragment to use with the new instance + * @return static A new instance with the specified fragment + */ + public function withFragment($fragment, int $flags = 0): UriInterface + { + $clone = clone $this; + $clone->setComponent('fragment', $fragment); + return $clone; + } + + + // ---------- PSR-7 string ---------------------------------------------------------------------------------------- + + /** + * Return the string representation as a URI reference. + * @return string + */ + public function __toString(): string + { + return $this->toString(); + } + + + // ---------- Extensions ------------------------------------------------------------------------------------------ + + /** + * Return the string representation as a URI reference. + * @return string + */ + public function toString(int $flags = 0): string + { + $scheme = $this->formatComponent($this->getComponent('scheme'), '', ':'); + $authority = $this->authority ? "//{$this->formatComponent($this->getAuthority($flags))}" : ''; + $path_flags = ($this->authority && $this->path ? self::ABSOLUTE_PATH : 0) | $flags; + $path = $this->formatComponent($this->getPath($path_flags)); + $query = $this->formatComponent($this->getComponent('query'), '?'); + $fragment = $this->formatComponent($this->getComponent('fragment'), '#'); + return "{$scheme}{$authority}{$path}{$query}{$fragment}"; + } + + + // ---------- Private helper methods ------------------------------------------------------------------------------ + + private function parse(string $uri_string = ''): void + { + if ($uri_string === '') { + return; + } + preg_match(self::RE_MAIN, $uri_string, $main); + $this->authority = !empty($main['authorityc']); + $this->setComponent('scheme', isset($main['schemec']) ? $main['scheme'] : ''); + $this->setComponent('path', isset($main['path']) ? $main['path'] : ''); + $this->setComponent('query', isset($main['queryc']) ? $main['query'] : ''); + $this->setComponent('fragment', isset($main['fragmentc']) ? $main['fragment'] : ''); + if ($this->authority) { + preg_match(self::RE_AUTH, $main['authority'], $auth); + if (empty($auth) && $main['authority'] !== '') { + throw new InvalidArgumentException("Invalid 'authority'."); + } + if ($this->isEmpty($auth['host']) && !$this->isEmpty($auth['user'])) { + throw new InvalidArgumentException("Invalid 'authority'."); + } + $this->setComponent('user', isset($auth['user']) ? $auth['user'] : ''); + $this->setComponent('pass', isset($auth['passc']) ? $auth['pass'] : ''); + $this->setComponent('host', isset($auth['host']) ? $auth['host'] : ''); + $this->setComponent('port', isset($auth['portc']) ? $auth['port'] : ''); + } + } + + private function encode(string $source, string $keep = ''): string + { + $exclude = "[^%\/:=&!\$'()*+,;@{$keep}]+"; + $exp = "/(%{$exclude})|({$exclude})/"; + return preg_replace_callback($exp, function ($matches) { + if ($e = preg_match('/^(%[0-9a-fA-F]{2})/', $matches[0], $m)) { + return substr($matches[0], 0, 3) . rawurlencode(substr($matches[0], 3)); + } else { + return rawurlencode($matches[0]); + } + }, $source); + } + + private function setComponent(string $component, $value): void + { + $value = $this->parseCompontent($component, $value); + $this->$component = $value; + } + + private function parseCompontent(string $component, $value) + { + if ($this->isEmpty($value)) { + return null; + } + switch ($component) { + case 'scheme': + $this->assertString($component, $value); + $this->assertpattern($component, $value, '/^[a-z][a-z0-9-+.]*$/i'); + return mb_strtolower($value); + case 'host': // IP-literal / IPv4address / reg-name + $this->assertString($component, $value); + $this->authority = $this->authority || !$this->isEmpty($value); + return mb_strtolower($value); + case 'port': + $this->assertInteger($component, $value); + if ($value < 0 || $value > 65535) { + throw new InvalidArgumentException("Invalid port number"); + } + return (int)$value; + case 'path': + $this->assertString($component, $value); + $value = $this->encode($value); + return $value; + case 'user': + case 'pass': + case 'query': + case 'fragment': + $this->assertString($component, $value); + $value = $this->encode($value, '?'); + return $value; + } + } + + private function getComponent(string $component) + { + return isset($this->$component) ? $this->$component : null; + } + + private function formatComponent($value, string $before = '', string $after = ''): string + { + return $this->isEmpty($value) ? '' : "{$before}{$value}{$after}"; + } + + private function isEmpty($value): bool + { + return is_null($value) || $value === ''; + } + + private function assertString(string $component, $value): void + { + if (!is_string($value)) { + throw new InvalidArgumentException("Invalid '{$component}': Should be a string"); + } + } + + private function assertInteger(string $component, $value): void + { + if (!is_numeric($value) || intval($value) != $value) { + throw new InvalidArgumentException("Invalid '{$component}': Should be an integer"); + } + } + + private function assertPattern(string $component, string $value, string $pattern): void + { + if (preg_match($pattern, $value) == 0) { + throw new InvalidArgumentException("Invalid '{$component}': Should match {$pattern}"); + } + } + + private function normalizePath(string $path): string + { + $result = []; + preg_match_all('!([^/]*/|[^/]*$)!', $path, $items); + foreach ($items[0] as $item) { + switch ($item) { + case '': + case './': + case '.': + break; // just skip + case '/': + if (empty($result)) { + array_push($result, $item); // add + } + break; + case '..': + case '../': + if (empty($result) || end($result) == '../') { + array_push($result, $item); // add + } else { + array_pop($result); // remove previous + } + break; + default: + array_push($result, $item); // add + } + } + return implode('', $result); + } + + private function idna(string $value): string + { + if ($value === '' || !is_callable('idn_to_ascii')) { + return $value; // Can't convert, but don't cause exception + } + return idn_to_ascii($value, IDNA_NONTRANSITIONAL_TO_ASCII, INTL_IDNA_VARIANT_UTS46); + } +} diff --git a/vendor/phrity/net-uri/src/UriFactory.php b/vendor/phrity/net-uri/src/UriFactory.php new file mode 100644 index 0000000..f100dd0 --- /dev/null +++ b/vendor/phrity/net-uri/src/UriFactory.php @@ -0,0 +1,31 @@ + Net > Uri + * @see https://www.rfc-editor.org/rfc/rfc3986 + * @see https://www.php-fig.org/psr/psr-17/#26-urifactoryinterface + */ + +namespace Phrity\Net; + +use Psr\Http\Message\{ + UriFactoryInterface, + UriInterface +}; + +/** + * Net\UriFactory class. + */ +class UriFactory implements UriFactoryInterface +{ + /** + * Create a new URI. + * @param string $uri The URI to parse. + * @throws \InvalidArgumentException If the given URI cannot be parsed + */ + public function createUri(string $uri = ''): UriInterface + { + return new Uri($uri); + } +} diff --git a/vendor/phrity/util-errorhandler/.github/workflows/acceptance.yml b/vendor/phrity/util-errorhandler/.github/workflows/acceptance.yml new file mode 100644 index 0000000..3eff1c1 --- /dev/null +++ b/vendor/phrity/util-errorhandler/.github/workflows/acceptance.yml @@ -0,0 +1,93 @@ +name: Acceptance + +on: [push, pull_request] + +jobs: + + test: + strategy: + matrix: + php-versions: ["7.4", "8.0", "8.1", "8.2", "8.3", "8.4"] + runs-on: ubuntu-latest + name: Unit test + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + coverage: none + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Get composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + - name: Cache dependencies + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer- + - name: Install dependencies + run: composer install --prefer-dist + - name: Test + run: vendor/bin/phpunit + + cs-check: + runs-on: ubuntu-latest + name: Code standard + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "8.3" + coverage: none + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Get composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + - name: Cache dependencies + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer- + - name: Install dependencies + run: composer install --prefer-dist + - name: Code standard + run: vendor/bin/phpcs --standard=PSR1,PSR12 --encoding=UTF-8 --report=full --colors src tests + + coverage: + runs-on: ubuntu-latest + name: Code coverage + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "8.3" + coverage: xdebug + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Get composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + - name: Cache dependencies + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer- + - name: Install dependencies + run: composer install --prefer-dist + - name: Code coverage build + run: XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-clover build/logs/clover.xml + - name: Code coverage upload + env: + COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: vendor/bin/php-coveralls -v diff --git a/vendor/phrity/util-errorhandler/.gitignore b/vendor/phrity/util-errorhandler/.gitignore new file mode 100644 index 0000000..379ab4b --- /dev/null +++ b/vendor/phrity/util-errorhandler/.gitignore @@ -0,0 +1,6 @@ +.DS_Store +.phpunit.result.cache +build/ +composer.lock +composer.phar +vendor/ \ No newline at end of file diff --git a/vendor/phrity/util-errorhandler/Makefile b/vendor/phrity/util-errorhandler/Makefile new file mode 100644 index 0000000..b97dcbd --- /dev/null +++ b/vendor/phrity/util-errorhandler/Makefile @@ -0,0 +1,41 @@ +# Default +all: deps-install + + +# DEPENDENCY MANAGEMENT + +# Updates dependencies according to lock file +deps-install: composer.phar + ./composer.phar --no-interaction install + +# Updates dependencies according to json file +deps-update: composer.phar + ./composer.phar self-update + ./composer.phar --no-interaction update + + +# TESTS AND REPORTS + +# Code standard check +cs-check: composer.lock + ./vendor/bin/phpcs --standard=PSR1,PSR12 --encoding=UTF-8 --report=full --colors src tests + +# Run tests +test: composer.lock + ./vendor/bin/phpunit + +# Run tests with clover coverage report +coverage: composer.lock + XDEBUG_MODE=coverage ./vendor/bin/phpunit --coverage-clover build/logs/clover.xml + ./vendor/bin/php-coveralls -v + + +# INITIAL INSTALL + +# Ensures composer is installed +composer.phar: + curl -sS https://getcomposer.org/installer | php + +# Ensures composer is installed and dependencies loaded +composer.lock: composer.phar + ./composer.phar --no-interaction install \ No newline at end of file diff --git a/vendor/phrity/util-errorhandler/README.md b/vendor/phrity/util-errorhandler/README.md new file mode 100644 index 0000000..a634491 --- /dev/null +++ b/vendor/phrity/util-errorhandler/README.md @@ -0,0 +1,147 @@ +[![Build Status](https://github.com/sirn-se/phrity-util-errorhandler/actions/workflows/acceptance.yml/badge.svg)](https://github.com/sirn-se/phrity-util-errorhandler/actions) +[![Coverage Status](https://coveralls.io/repos/github/sirn-se/phrity-util-errorhandler/badge.svg?branch=main)](https://coveralls.io/github/sirn-se/phrity-util-errorhandler?branch=main) + +# Error Handler utility + +The PHP [error handling](https://www.php.net/manual/en/book.errorfunc.php) can be somewhat of a headache. +Typically an application uses a system level [error handler](https://www.php.net/manual/en/function.set-error-handler.php) and/or suppressing errors using the `@` prefix. +But those cases when your code need to act on triggered errors are more tricky. + +This library provides two convenience methods to handle errors on code blocks, either by throwing exceptions or running callback code when an error occurs. + +Current version supports PHP `^7.2|^8.0`. + +## Installation + +Install with [Composer](https://getcomposer.org/); +``` +composer require phrity/util-errorhandler +``` + +## The Error Handler + +The class provides two main methods; `with()` and `withAll()`. +The difference is that `with()` will act immediately on an error and abort further code execution, while `withAll()` will attempt to execute the entire code block before acting on errors that occurred. + +### Throwing ErrorException + +```php +use Phrity\Util\ErrorHandler; + +$handler = new ErrorHandler(); +$result = $handler->with(function () { + // Code to execute + return $success_result; +}); +$result = $handler->withAll(function () { + // Code to execute + return $success_result; +}); +``` +The examples above will run the callback code, but if an error occurs it will throw an [ErrorException](https://www.php.net/manual/en/class.errorexception.php). +Error message and severity will be that of the triggering error. +* `with()` will throw immediately when occured +* `withAll()` will throw when code is complete; if more than one error occurred, the first will be thrown + +### Throwing specified Throwable + +```php +use Phrity\Util\ErrorHandler; + +$handler = new ErrorHandler(); +$result = $handler->with(function () { + // Code to execute + return $success_result; +}, new RuntimeException('A specified error')); +$result = $handler->withAll(function () { + // Code to execute + return $success_result; +}, new RuntimeException('A specified error')); +``` +The examples above will run the callback code, but if an error occurs it will throw provided Throwable. +The thrown Throwable will have an [ErrorException](https://www.php.net/manual/en/class.errorexception.php) attached as `$previous`. +* `with()` will throw immediately when occured +* `withAll()` will throw when code is complete; if more than one error occurred, the first will be thrown + +### Using callback + +```php +use Phrity\Util\ErrorHandler; + +$handler = new ErrorHandler(); +$result = $handler->with(function () { + // Code to execute + return $success_result; +}, function (ErrorException $error) { + // Code to handle error + return $error_result; +}); +$result = $handler->withAll(function () { + // Code to execute + return $success_result; +}, function (array $errors, $success_result) { + // Code to handle errors + return $error_result; +}); +``` +The examples above will run the callback code, but if an error occurs it will call the error callback as well. +* `with()` will run the error callback immediately when error occured; error callback expects an ErrorException instance +* `withAll()` will run the error callback when code is complete; error callback expects an array of ErrorException and the returned result of code callback + +### Filtering error types + +Both `with()` and `withAll()` accepts error level(s) as last parameter. +```php +use Phrity\Util\ErrorHandler; + +$handler = new ErrorHandler(); +$result = $handler->with(function () { + // Code to execute + return $success_result; +}, null, E_USER_ERROR); +$result = $handler->withAll(function () { + // Code to execute + return $success_result; +}, null, E_USER_ERROR & E_USER_WARNING); +``` +Any value or combination of values accepted by [set_error_handler](https://www.php.net/manual/en/function.set-error-handler.php) is usable. +Default is `E_ALL`. [List of constants](https://www.php.net/manual/en/errorfunc.constants.php). + +### The global error handler + +The class also has global `set()` and `restore()` methods. + +```php +use Phrity\Util\ErrorHandler; + +$handler = new ErrorHandler(); +$handler->set(); // Throws ErrorException on error +$handler->set(new RuntimeException('A specified error')); // Throws provided Throwable on error +$handler->set(function (ErrorException $error) { + // Code to handle errors + return $error_result; +}); // Runs callback on error +$handler->restore(); // Restores error handler +``` + +### Class synopsis + +```php +Phrity\Util\ErrorHandler { + + /* Methods */ + public __construct() + + public with(callable $callback, mixed $handling = null, int $levels = E_ALL) : mixed + public withAll(callable $callback, mixed $handling = null, int $levels = E_ALL) : mixed + public set($handling = null, int $levels = E_ALL) : mixed + public restore() : bool +} +``` + +## Versions + +| Version | PHP | | +| --- | --- | --- | +| `1.1` | `^7.4\|^8.0` | Some improvements | +| `1.0` | `^7.2\|^8.0` | Initial version | diff --git a/vendor/phrity/util-errorhandler/composer.json b/vendor/phrity/util-errorhandler/composer.json new file mode 100644 index 0000000..22862b6 --- /dev/null +++ b/vendor/phrity/util-errorhandler/composer.json @@ -0,0 +1,33 @@ +{ + "name": "phrity/util-errorhandler", + "type": "library", + "description": "Inline error handler; catch and resolve errors for code block.", + "homepage": "https://phrity.sirn.se/util-errorhandler", + "keywords": ["error", "warning"], + "license": "MIT", + "authors": [ + { + "name": "Sören Jensen", + "email": "sirn@sirn.se", + "homepage": "https://phrity.sirn.se" + } + ], + "autoload": { + "psr-4": { + "Phrity\\Util\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Phrity\\Util\\Tests\\": "tests/" + } + }, + "require": { + "php": "^7.4 | ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0 | ^10.0 | ^11.0", + "php-coveralls/php-coveralls": "^2.0", + "squizlabs/php_codesniffer": "^3.5" + } +} diff --git a/vendor/phrity/util-errorhandler/phpunit.xml.dist b/vendor/phrity/util-errorhandler/phpunit.xml.dist new file mode 100644 index 0000000..eebfbe3 --- /dev/null +++ b/vendor/phrity/util-errorhandler/phpunit.xml.dist @@ -0,0 +1,13 @@ + + + + + ./tests/ + + + + + ./src/ + + + diff --git a/vendor/phrity/util-errorhandler/src/ErrorHandler.php b/vendor/phrity/util-errorhandler/src/ErrorHandler.php new file mode 100644 index 0000000..fa81780 --- /dev/null +++ b/vendor/phrity/util-errorhandler/src/ErrorHandler.php @@ -0,0 +1,121 @@ + Util > ErrorHandler + */ + +namespace Phrity\Util; + +use ErrorException; +use Throwable; + +/** + * ErrorHandler utility class. + * Allows catching and resolving errors inline. + */ +class ErrorHandler +{ + /* ----------------- Public methods ---------------------------------------------- */ + + /** + * Set error handler to run until removed. + * @param mixed $handling + * - If null, handler will throw ErrorException + * - If Throwable $t, throw $t with ErrorException attached as previous + * - If callable, will invoke callback with ErrorException as argument + * @param int $levels Error levels to catch, all errors by default + * @return mixed Previously registered error handler, if any + */ + public function set($handling = null, int $levels = E_ALL) + { + return set_error_handler($this->getHandler($handling), $levels); + } + + /** + * Remove error handler. + * @return bool True if removed + */ + public function restore(): bool + { + return restore_error_handler(); + } + + /** + * Run code with error handling, breaks on first encountered error. + * @param callable $callback The code to run + * @param mixed $handling + * - If null, handler will throw ErrorException + * - If Throwable $t, throw $t with ErrorException attached as previous + * - If callable, will invoke callback with ErrorException as argument + * @param int $levels Error levels to catch, all errors by default + * @return mixed Return what $callback returns, or what $handling retuns on error + */ + public function with(callable $callback, $handling = null, int $levels = E_ALL) + { + $error = null; + $result = null; + try { + $this->set(null, $levels); + $result = $callback(); + } catch (ErrorException $e) { + $error = $this->handle($handling, $e); + } finally { + $this->restore(); + } + return $error ?? $result; + } + + /** + * Run code with error handling, comletes code before handling errors + * @param callable $callback The code to run + * @param mixed $handling + * - If null, handler will throw ErrorException + * - If Throwable $t, throw $t with ErrorException attached as previous + * - If callable, will invoke callback with ErrorException as argument + * @param int $levels Error levels to catch, all errors by default + * @return mixed Return what $callback returns, or what $handling retuns on error + */ + public function withAll(callable $callback, $handling = null, int $levels = E_ALL) + { + $errors = []; + $this->set(function (ErrorException $e) use (&$errors) { + $errors[] = $e; + }, $levels); + $result = $callback(); + $this->restore(); + $error = empty($errors) ? null : $this->handle($handling, $errors, $result); + return $error ?? $result; + } + + + /* ----------------- Private helpers --------------------------------------------- */ + + // Get handler function + private function getHandler($handling) + { + return function ($severity, $message, $file, $line) use ($handling) { + $error = new ErrorException($message, 0, $severity, $file, $line); + $this->handle($handling, $error); + }; + } + + // Handle error according to $handlig type + private function handle($handling, $error, $result = null) + { + if (is_callable($handling)) { + return $handling($error, $result); + } + if (is_array($error)) { + $error = array_shift($error); + } + if ($handling instanceof Throwable) { + try { + throw $error; + } finally { + throw $handling; + } + } + throw $error; + } +} diff --git a/vendor/phrity/util-errorhandler/tests/ErrorHandlerTest.php b/vendor/phrity/util-errorhandler/tests/ErrorHandlerTest.php new file mode 100644 index 0000000..31e0f49 --- /dev/null +++ b/vendor/phrity/util-errorhandler/tests/ErrorHandlerTest.php @@ -0,0 +1,303 @@ + Util > ErrorHandler + */ + +declare(strict_types=1); + +namespace Phrity\Util; + +use ErrorException; +use RuntimeException; +use Phrity\Util\ErrorHandler; +use PHPUnit\Framework\TestCase; + +/** + * ErrorHandler test class. + */ +class ErrorHandlerTest extends TestCase +{ + /** + * Set up for all tests + */ + public function setUp(): void + { + error_reporting(-1); + } + + public function testSetNull(): void + { + $handler = new ErrorHandler(); + $handler->set(); + + // Verify exception + try { + trigger_error('An error'); + } catch (ErrorException $e) { + $this->assertEquals('An error', $e->getMessage()); + $this->assertEquals(0, $e->getCode()); + $this->assertEquals(E_USER_NOTICE, $e->getSeverity()); + $this->assertNull($e->getPrevious()); + } + + // Restore handler + $this->assertTrue($handler->restore()); + } + + public function testSetThrowable(): void + { + $handler = new ErrorHandler(); + $handler->set(new RuntimeException('A provided exception', 23)); + + // Verify exception + try { + trigger_error('An error'); + } catch (RuntimeException $e) { + $this->assertEquals('A provided exception', $e->getMessage()); + $this->assertEquals(23, $e->getCode()); + $this->assertNotNull($e->getPrevious()); + $prev = $e->getPrevious(); + $this->assertEquals('An error', $prev->getMessage()); + $this->assertEquals(0, $prev->getCode()); + $this->assertEquals(E_USER_NOTICE, $prev->getSeverity()); + $this->assertNull($prev->getPrevious()); + } + + // Restore handler + $this->assertTrue($handler->restore()); + } + + public function testSetCallback(): void + { + $handler = new ErrorHandler(); + $result = null; + $handler->set(function ($error) use (&$result) { + $result = [ + 'message' => $error->getMessage(), + 'code' => $error->getCode(), + 'severity' => $error->getSeverity(), + ]; + }); + + // Verify exception + trigger_error('An error'); + $this->assertEquals([ + 'message' => 'An error', + 'code' => 0, + 'severity' => E_USER_NOTICE, + ], $result); + + // Restore handler + $this->assertTrue($handler->restore()); + } + + public function testWithNull(): void + { + $handler = new ErrorHandler(); + $check = false; + + // No exception + $result = $handler->with(function () { + return 'Code success'; + }); + $this->assertEquals('Code success', $result); + + // Verify exception + try { + $result = $handler->with(function () use (&$check) { + trigger_error('An error'); + $check = true; + return 'Code success'; + }); + } catch (ErrorException $e) { + $this->assertEquals('An error', $e->getMessage()); + $this->assertEquals(0, $e->getCode()); + $this->assertEquals(E_USER_NOTICE, $e->getSeverity()); + $this->assertNull($e->getPrevious()); + } + $this->assertFalse($check); + + // Verify that exception is thrown + $this->expectException('ErrorException'); + $result = $handler->with(function () { + trigger_error('An error'); + return 'Code success'; + }); + } + + public function testWithThrowable(): void + { + $handler = new ErrorHandler(); + $check = false; + + // No exception + $result = $handler->with(function () { + return 'Code success'; + }); + $this->assertEquals('Code success', $result); + + // Verify exception + try { + $result = $handler->with(function () use (&$check) { + trigger_error('An error'); + $check = true; + return 'Code success'; + }, new RuntimeException('A provided exception', 23)); + } catch (RuntimeException $e) { + $this->assertEquals('A provided exception', $e->getMessage()); + $this->assertEquals(23, $e->getCode()); + $this->assertNotNull($e->getPrevious()); + $prev = $e->getPrevious(); + $this->assertEquals('An error', $prev->getMessage()); + $this->assertEquals(0, $prev->getCode()); + $this->assertEquals(E_USER_NOTICE, $prev->getSeverity()); + $this->assertNull($prev->getPrevious()); + } + $this->assertFalse($check); + + // Verify that exception is thrown + $this->expectException('RuntimeException'); + $result = $handler->with(function () { + trigger_error('An error'); + return 'Code success'; + }, new RuntimeException('A provided exception', 23)); + } + + public function testWithCallback(): void + { + $handler = new ErrorHandler(); + $check = false; + + // No error invoked + $result = $handler->with(function () { + return 'Code success'; + }, function ($error) { + return $error; + }); + $this->assertEquals('Code success', $result); + + // An error is invoked + $result = $handler->with(function () use (&$check) { + trigger_error('An error'); + $check = true; + return 'Code success'; + }, function ($error) { + return $error; + }); + $this->assertFalse($check); + + $this->assertEquals('An error', $result->getMessage()); + $this->assertEquals(0, $result->getCode()); + $this->assertEquals(E_USER_NOTICE, $result->getSeverity()); + $this->assertNull($result->getPrevious()); + } + + public function testWithAllNull(): void + { + $handler = new ErrorHandler(); + $check = false; + + // No error invoked + $result = $handler->withAll(function () { + return 'Code success'; + }); + $this->assertEquals('Code success', $result); + + // Verify exception + try { + $result = $handler->withAll(function () use (&$check) { + trigger_error('An error'); + $check = true; + return 'Code success'; + }); + } catch (ErrorException $e) { + $this->assertEquals('An error', $e->getMessage()); + $this->assertEquals(0, $e->getCode()); + $this->assertEquals(E_USER_NOTICE, $e->getSeverity()); + $this->assertNull($e->getPrevious()); + } + $this->assertTrue($check); + + // Verify that exception is thrown + $this->expectException('ErrorException'); + $result = $handler->withAll(function () { + trigger_error('An error'); + return 'Code success'; + }); + } + + public function testWithAllThrowable(): void + { + $handler = new ErrorHandler(); + $check = false; + + // No exception + $result = $handler->withAll(function () { + return 'Code success'; + }); + $this->assertEquals('Code success', $result); + + // Verify exception + try { + $result = $handler->withAll(function () use (&$check) { + trigger_error('An error'); + $check = true; + return 'Code success'; + }, new RuntimeException('A provided exception', 23)); + } catch (RuntimeException $e) { + $this->assertEquals('A provided exception', $e->getMessage()); + $this->assertEquals(23, $e->getCode()); + $this->assertNotNull($e->getPrevious()); + $prev = $e->getPrevious(); + $this->assertEquals('An error', $prev->getMessage()); + $this->assertEquals(0, $prev->getCode()); + $this->assertEquals(E_USER_NOTICE, $prev->getSeverity()); + $this->assertNull($prev->getPrevious()); + } + $this->assertTrue($check); + + // Verify that exception is thrown + $this->expectException('RuntimeException'); + $result = $handler->withAll(function () { + trigger_error('An error'); + return 'Code success'; + }, new RuntimeException('A provided exception', 23)); + } + + public function testWithAllCallback(): void + { + $handler = new ErrorHandler(); + $check = false; + + // No error invoked + $result = $handler->withAll(function () { + return 'Code success'; + }, function ($error, $result) { + return $error; + }); + $this->assertEquals('Code success', $result); + + // An error is invoked + $result = $handler->withAll(function () use (&$check) { + trigger_error('An error'); + trigger_error('Another error', E_USER_WARNING); + $check = true; + return 'Code success'; + }, function ($errors, $result) { + return ['errors' => $errors, 'result' => $result]; + }); + $this->assertTrue($check); + + $this->assertEquals('Code success', $result['result']); + $this->assertEquals('An error', $result['errors'][0]->getMessage()); + $this->assertEquals(0, $result['errors'][0]->getCode()); + $this->assertEquals(E_USER_NOTICE, $result['errors'][0]->getSeverity()); + $this->assertNull($result['errors'][0]->getPrevious()); + $this->assertEquals('Another error', $result['errors'][1]->getMessage()); + $this->assertEquals(0, $result['errors'][1]->getCode()); + $this->assertEquals(E_USER_WARNING, $result['errors'][1]->getSeverity()); + $this->assertNull($result['errors'][1]->getPrevious()); + } +} diff --git a/vendor/psr/http-factory/LICENSE b/vendor/psr/http-factory/LICENSE new file mode 100644 index 0000000..3f1559b --- /dev/null +++ b/vendor/psr/http-factory/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 PHP-FIG + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/psr/http-factory/README.md b/vendor/psr/http-factory/README.md new file mode 100644 index 0000000..bf8913b --- /dev/null +++ b/vendor/psr/http-factory/README.md @@ -0,0 +1,12 @@ +HTTP Factories +============== + +This repository holds all interfaces related to [PSR-17 (HTTP Factories)][psr-url]. + +Note that this is not a HTTP Factory implementation of its own. It is merely interfaces that describe the components of a HTTP Factory. + +The installable [package][package-url] and [implementations][implementation-url] are listed on Packagist. + +[psr-url]: https://www.php-fig.org/psr/psr-17/ +[package-url]: https://packagist.org/packages/psr/http-factory +[implementation-url]: https://packagist.org/providers/psr/http-factory-implementation diff --git a/vendor/psr/http-factory/composer.json b/vendor/psr/http-factory/composer.json new file mode 100644 index 0000000..e9a3967 --- /dev/null +++ b/vendor/psr/http-factory/composer.json @@ -0,0 +1,38 @@ +{ + "name": "psr/http-factory", + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "psr", + "psr-7", + "psr-17", + "http", + "factory", + "message", + "request", + "response" + ], + "license": "MIT", + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "require": { + "php": ">=7.0.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + } +} diff --git a/vendor/psr/http-factory/src/RequestFactoryInterface.php b/vendor/psr/http-factory/src/RequestFactoryInterface.php new file mode 100644 index 0000000..cb39a08 --- /dev/null +++ b/vendor/psr/http-factory/src/RequestFactoryInterface.php @@ -0,0 +1,18 @@ + `RequestInterface`, `ServerRequestInterface`, `ResponseInterface` extend `MessageInterface` because the `Request` and the `Response` are `HTTP Messages`. +> When using `ServerRequestInterface`, both `RequestInterface` and `Psr\Http\Message\MessageInterface` methods are considered. + diff --git a/vendor/psr/http-message/docs/PSR7-Usage.md b/vendor/psr/http-message/docs/PSR7-Usage.md new file mode 100644 index 0000000..b6d048a --- /dev/null +++ b/vendor/psr/http-message/docs/PSR7-Usage.md @@ -0,0 +1,159 @@ +### PSR-7 Usage + +All PSR-7 applications comply with these interfaces +They were created to establish a standard between middleware implementations. + +> `RequestInterface`, `ServerRequestInterface`, `ResponseInterface` extend `MessageInterface` because the `Request` and the `Response` are `HTTP Messages`. +> When using `ServerRequestInterface`, both `RequestInterface` and `Psr\Http\Message\MessageInterface` methods are considered. + + +The following examples will illustrate how basic operations are done in PSR-7. + +##### Examples + + +For this examples to work (at least) a PSR-7 implementation package is required. (eg: zendframework/zend-diactoros, guzzlehttp/psr7, slim/slim, etc) +All PSR-7 implementations should have the same behaviour. + +The following will be assumed: +`$request` is an object of `Psr\Http\Message\RequestInterface` and + +`$response` is an object implementing `Psr\Http\Message\RequestInterface` + + +### Working with HTTP Headers + +#### Adding headers to response: + +```php +$response->withHeader('My-Custom-Header', 'My Custom Message'); +``` + +#### Appending values to headers + +```php +$response->withAddedHeader('My-Custom-Header', 'The second message'); +``` + +#### Checking if header exists: + +```php +$request->hasHeader('My-Custom-Header'); // will return false +$response->hasHeader('My-Custom-Header'); // will return true +``` + +> Note: My-Custom-Header was only added in the Response + +#### Getting comma-separated values from a header (also applies to request) + +```php +// getting value from request headers +$request->getHeaderLine('Content-Type'); // will return: "text/html; charset=UTF-8" +// getting value from response headers +$response->getHeaderLine('My-Custom-Header'); // will return: "My Custom Message; The second message" +``` + +#### Getting array of value from a header (also applies to request) +```php +// getting value from request headers +$request->getHeader('Content-Type'); // will return: ["text/html", "charset=UTF-8"] +// getting value from response headers +$response->getHeader('My-Custom-Header'); // will return: ["My Custom Message", "The second message"] +``` + +#### Removing headers from HTTP Messages +```php +// removing a header from Request, removing deprecated "Content-MD5" header +$request->withoutHeader('Content-MD5'); + +// removing a header from Response +// effect: the browser won't know the size of the stream +// the browser will download the stream till it ends +$response->withoutHeader('Content-Length'); +``` + +### Working with HTTP Message Body + +When working with the PSR-7 there are two methods of implementation: +#### 1. Getting the body separately + +> This method makes the body handling easier to understand and is useful when repeatedly calling body methods. (You only call `getBody()` once). Using this method mistakes like `$response->write()` are also prevented. + +```php +$body = $response->getBody(); +// operations on body, eg. read, write, seek +// ... +// replacing the old body +$response->withBody($body); +// this last statement is optional as we working with objects +// in this case the "new" body is same with the "old" one +// the $body variable has the same value as the one in $request, only the reference is passed +``` + +#### 2. Working directly on response + +> This method is useful when only performing few operations as the `$request->getBody()` statement fragment is required + +```php +$response->getBody()->write('hello'); +``` + +### Getting the body contents + +The following snippet gets the contents of a stream contents. +> Note: Streams must be rewinded, if content was written into streams, it will be ignored when calling `getContents()` because the stream pointer is set to the last character, which is `\0` - meaning end of stream. +```php +$body = $response->getBody(); +$body->rewind(); // or $body->seek(0); +$bodyText = $body->getContents(); +``` +> Note: If `$body->seek(1)` is called before `$body->getContents()`, the first character will be ommited as the starting pointer is set to `1`, not `0`. This is why using `$body->rewind()` is recommended. + +### Append to body + +```php +$response->getBody()->write('Hello'); // writing directly +$body = $request->getBody(); // which is a `StreamInterface` +$body->write('xxxxx'); +``` + +### Prepend to body +Prepending is different when it comes to streams. The content must be copied before writing the content to be prepended. +The following example will explain the behaviour of streams. + +```php +// assuming our response is initially empty +$body = $repsonse->getBody(); +// writing the string "abcd" +$body->write('abcd'); + +// seeking to start of stream +$body->seek(0); +// writing 'ef' +$body->write('ef'); // at this point the stream contains "efcd" +``` + +#### Prepending by rewriting separately + +```php +// assuming our response body stream only contains: "abcd" +$body = $response->getBody(); +$body->rewind(); +$contents = $body->getContents(); // abcd +// seeking the stream to beginning +$body->rewind(); +$body->write('ef'); // stream contains "efcd" +$body->write($contents); // stream contains "efabcd" +``` + +> Note: `getContents()` seeks the stream while reading it, therefore if the second `rewind()` method call was not present the stream would have resulted in `abcdefabcd` because the `write()` method appends to stream if not preceeded by `rewind()` or `seek(0)`. + +#### Prepending by using contents as a string +```php +$body = $response->getBody(); +$body->rewind(); +$contents = $body->getContents(); // efabcd +$contents = 'ef'.$contents; +$body->rewind(); +$body->write($contents); +``` diff --git a/vendor/psr/http-message/src/MessageInterface.php b/vendor/psr/http-message/src/MessageInterface.php new file mode 100644 index 0000000..8cdb4ed --- /dev/null +++ b/vendor/psr/http-message/src/MessageInterface.php @@ -0,0 +1,189 @@ +getHeaders() as $name => $values) { + * echo $name . ": " . implode(", ", $values); + * } + * + * // Emit headers iteratively: + * foreach ($message->getHeaders() as $name => $values) { + * foreach ($values as $value) { + * header(sprintf('%s: %s', $name, $value), false); + * } + * } + * + * While header names are not case-sensitive, getHeaders() will preserve the + * exact case in which headers were originally specified. + * + * @return string[][] Returns an associative array of the message's headers. Each + * key MUST be a header name, and each value MUST be an array of strings + * for that header. + */ + public function getHeaders(); + + /** + * Checks if a header exists by the given case-insensitive name. + * + * @param string $name Case-insensitive header field name. + * @return bool Returns true if any header names match the given header + * name using a case-insensitive string comparison. Returns false if + * no matching header name is found in the message. + */ + public function hasHeader(string $name); + + /** + * Retrieves a message header value by the given case-insensitive name. + * + * This method returns an array of all the header values of the given + * case-insensitive header name. + * + * If the header does not appear in the message, this method MUST return an + * empty array. + * + * @param string $name Case-insensitive header field name. + * @return string[] An array of string values as provided for the given + * header. If the header does not appear in the message, this method MUST + * return an empty array. + */ + public function getHeader(string $name); + + /** + * Retrieves a comma-separated string of the values for a single header. + * + * This method returns all of the header values of the given + * case-insensitive header name as a string concatenated together using + * a comma. + * + * NOTE: Not all header values may be appropriately represented using + * comma concatenation. For such headers, use getHeader() instead + * and supply your own delimiter when concatenating. + * + * If the header does not appear in the message, this method MUST return + * an empty string. + * + * @param string $name Case-insensitive header field name. + * @return string A string of values as provided for the given header + * concatenated together using a comma. If the header does not appear in + * the message, this method MUST return an empty string. + */ + public function getHeaderLine(string $name); + + /** + * Return an instance with the provided value replacing the specified header. + * + * While header names are case-insensitive, the casing of the header will + * be preserved by this function, and returned from getHeaders(). + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * new and/or updated header and value. + * + * @param string $name Case-insensitive header field name. + * @param string|string[] $value Header value(s). + * @return static + * @throws \InvalidArgumentException for invalid header names or values. + */ + public function withHeader(string $name, $value); + + /** + * Return an instance with the specified header appended with the given value. + * + * Existing values for the specified header will be maintained. The new + * value(s) will be appended to the existing list. If the header did not + * exist previously, it will be added. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * new header and/or value. + * + * @param string $name Case-insensitive header field name to add. + * @param string|string[] $value Header value(s). + * @return static + * @throws \InvalidArgumentException for invalid header names or values. + */ + public function withAddedHeader(string $name, $value); + + /** + * Return an instance without the specified header. + * + * Header resolution MUST be done without case-sensitivity. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that removes + * the named header. + * + * @param string $name Case-insensitive header field name to remove. + * @return static + */ + public function withoutHeader(string $name); + + /** + * Gets the body of the message. + * + * @return StreamInterface Returns the body as a stream. + */ + public function getBody(); + + /** + * Return an instance with the specified message body. + * + * The body MUST be a StreamInterface object. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return a new instance that has the + * new body stream. + * + * @param StreamInterface $body Body. + * @return static + * @throws \InvalidArgumentException When the body is not valid. + */ + public function withBody(StreamInterface $body); +} diff --git a/vendor/psr/http-message/src/RequestInterface.php b/vendor/psr/http-message/src/RequestInterface.php new file mode 100644 index 0000000..38066df --- /dev/null +++ b/vendor/psr/http-message/src/RequestInterface.php @@ -0,0 +1,131 @@ +getQuery()` + * or from the `QUERY_STRING` server param. + * + * @return array + */ + public function getQueryParams(); + + /** + * Return an instance with the specified query string arguments. + * + * These values SHOULD remain immutable over the course of the incoming + * request. They MAY be injected during instantiation, such as from PHP's + * $_GET superglobal, or MAY be derived from some other value such as the + * URI. In cases where the arguments are parsed from the URI, the data + * MUST be compatible with what PHP's parse_str() would return for + * purposes of how duplicate query parameters are handled, and how nested + * sets are handled. + * + * Setting query string arguments MUST NOT change the URI stored by the + * request, nor the values in the server params. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated query string arguments. + * + * @param array $query Array of query string arguments, typically from + * $_GET. + * @return static + */ + public function withQueryParams(array $query); + + /** + * Retrieve normalized file upload data. + * + * This method returns upload metadata in a normalized tree, with each leaf + * an instance of Psr\Http\Message\UploadedFileInterface. + * + * These values MAY be prepared from $_FILES or the message body during + * instantiation, or MAY be injected via withUploadedFiles(). + * + * @return array An array tree of UploadedFileInterface instances; an empty + * array MUST be returned if no data is present. + */ + public function getUploadedFiles(); + + /** + * Create a new instance with the specified uploaded files. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated body parameters. + * + * @param array $uploadedFiles An array tree of UploadedFileInterface instances. + * @return static + * @throws \InvalidArgumentException if an invalid structure is provided. + */ + public function withUploadedFiles(array $uploadedFiles); + + /** + * Retrieve any parameters provided in the request body. + * + * If the request Content-Type is either application/x-www-form-urlencoded + * or multipart/form-data, and the request method is POST, this method MUST + * return the contents of $_POST. + * + * Otherwise, this method may return any results of deserializing + * the request body content; as parsing returns structured content, the + * potential types MUST be arrays or objects only. A null value indicates + * the absence of body content. + * + * @return null|array|object The deserialized body parameters, if any. + * These will typically be an array or object. + */ + public function getParsedBody(); + + /** + * Return an instance with the specified body parameters. + * + * These MAY be injected during instantiation. + * + * If the request Content-Type is either application/x-www-form-urlencoded + * or multipart/form-data, and the request method is POST, use this method + * ONLY to inject the contents of $_POST. + * + * The data IS NOT REQUIRED to come from $_POST, but MUST be the results of + * deserializing the request body content. Deserialization/parsing returns + * structured data, and, as such, this method ONLY accepts arrays or objects, + * or a null value if nothing was available to parse. + * + * As an example, if content negotiation determines that the request data + * is a JSON payload, this method could be used to create a request + * instance with the deserialized parameters. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated body parameters. + * + * @param null|array|object $data The deserialized body data. This will + * typically be in an array or object. + * @return static + * @throws \InvalidArgumentException if an unsupported argument type is + * provided. + */ + public function withParsedBody($data); + + /** + * Retrieve attributes derived from the request. + * + * The request "attributes" may be used to allow injection of any + * parameters derived from the request: e.g., the results of path + * match operations; the results of decrypting cookies; the results of + * deserializing non-form-encoded message bodies; etc. Attributes + * will be application and request specific, and CAN be mutable. + * + * @return array Attributes derived from the request. + */ + public function getAttributes(); + + /** + * Retrieve a single derived request attribute. + * + * Retrieves a single derived request attribute as described in + * getAttributes(). If the attribute has not been previously set, returns + * the default value as provided. + * + * This method obviates the need for a hasAttribute() method, as it allows + * specifying a default value to return if the attribute is not found. + * + * @see getAttributes() + * @param string $name The attribute name. + * @param mixed $default Default value to return if the attribute does not exist. + * @return mixed + */ + public function getAttribute(string $name, $default = null); + + /** + * Return an instance with the specified derived request attribute. + * + * This method allows setting a single derived request attribute as + * described in getAttributes(). + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated attribute. + * + * @see getAttributes() + * @param string $name The attribute name. + * @param mixed $value The value of the attribute. + * @return static + */ + public function withAttribute(string $name, $value); + + /** + * Return an instance that removes the specified derived request attribute. + * + * This method allows removing a single derived request attribute as + * described in getAttributes(). + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that removes + * the attribute. + * + * @see getAttributes() + * @param string $name The attribute name. + * @return static + */ + public function withoutAttribute(string $name); +} diff --git a/vendor/psr/http-message/src/StreamInterface.php b/vendor/psr/http-message/src/StreamInterface.php new file mode 100644 index 0000000..5924663 --- /dev/null +++ b/vendor/psr/http-message/src/StreamInterface.php @@ -0,0 +1,160 @@ + + * [user-info@]host[:port] + * + * + * If the port component is not set or is the standard port for the current + * scheme, it SHOULD NOT be included. + * + * @see https://tools.ietf.org/html/rfc3986#section-3.2 + * @return string The URI authority, in "[user-info@]host[:port]" format. + */ + public function getAuthority(); + + /** + * Retrieve the user information component of the URI. + * + * If no user information is present, this method MUST return an empty + * string. + * + * If a user is present in the URI, this will return that value; + * additionally, if the password is also present, it will be appended to the + * user value, with a colon (":") separating the values. + * + * The trailing "@" character is not part of the user information and MUST + * NOT be added. + * + * @return string The URI user information, in "username[:password]" format. + */ + public function getUserInfo(); + + /** + * Retrieve the host component of the URI. + * + * If no host is present, this method MUST return an empty string. + * + * The value returned MUST be normalized to lowercase, per RFC 3986 + * Section 3.2.2. + * + * @see http://tools.ietf.org/html/rfc3986#section-3.2.2 + * @return string The URI host. + */ + public function getHost(); + + /** + * Retrieve the port component of the URI. + * + * If a port is present, and it is non-standard for the current scheme, + * this method MUST return it as an integer. If the port is the standard port + * used with the current scheme, this method SHOULD return null. + * + * If no port is present, and no scheme is present, this method MUST return + * a null value. + * + * If no port is present, but a scheme is present, this method MAY return + * the standard port for that scheme, but SHOULD return null. + * + * @return null|int The URI port. + */ + public function getPort(); + + /** + * Retrieve the path component of the URI. + * + * The path can either be empty or absolute (starting with a slash) or + * rootless (not starting with a slash). Implementations MUST support all + * three syntaxes. + * + * Normally, the empty path "" and absolute path "/" are considered equal as + * defined in RFC 7230 Section 2.7.3. But this method MUST NOT automatically + * do this normalization because in contexts with a trimmed base path, e.g. + * the front controller, this difference becomes significant. It's the task + * of the user to handle both "" and "/". + * + * The value returned MUST be percent-encoded, but MUST NOT double-encode + * any characters. To determine what characters to encode, please refer to + * RFC 3986, Sections 2 and 3.3. + * + * As an example, if the value should include a slash ("/") not intended as + * delimiter between path segments, that value MUST be passed in encoded + * form (e.g., "%2F") to the instance. + * + * @see https://tools.ietf.org/html/rfc3986#section-2 + * @see https://tools.ietf.org/html/rfc3986#section-3.3 + * @return string The URI path. + */ + public function getPath(); + + /** + * Retrieve the query string of the URI. + * + * If no query string is present, this method MUST return an empty string. + * + * The leading "?" character is not part of the query and MUST NOT be + * added. + * + * The value returned MUST be percent-encoded, but MUST NOT double-encode + * any characters. To determine what characters to encode, please refer to + * RFC 3986, Sections 2 and 3.4. + * + * As an example, if a value in a key/value pair of the query string should + * include an ampersand ("&") not intended as a delimiter between values, + * that value MUST be passed in encoded form (e.g., "%26") to the instance. + * + * @see https://tools.ietf.org/html/rfc3986#section-2 + * @see https://tools.ietf.org/html/rfc3986#section-3.4 + * @return string The URI query string. + */ + public function getQuery(); + + /** + * Retrieve the fragment component of the URI. + * + * If no fragment is present, this method MUST return an empty string. + * + * The leading "#" character is not part of the fragment and MUST NOT be + * added. + * + * The value returned MUST be percent-encoded, but MUST NOT double-encode + * any characters. To determine what characters to encode, please refer to + * RFC 3986, Sections 2 and 3.5. + * + * @see https://tools.ietf.org/html/rfc3986#section-2 + * @see https://tools.ietf.org/html/rfc3986#section-3.5 + * @return string The URI fragment. + */ + public function getFragment(); + + /** + * Return an instance with the specified scheme. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified scheme. + * + * Implementations MUST support the schemes "http" and "https" case + * insensitively, and MAY accommodate other schemes if required. + * + * An empty scheme is equivalent to removing the scheme. + * + * @param string $scheme The scheme to use with the new instance. + * @return static A new instance with the specified scheme. + * @throws \InvalidArgumentException for invalid or unsupported schemes. + */ + public function withScheme(string $scheme); + + /** + * Return an instance with the specified user information. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified user information. + * + * Password is optional, but the user information MUST include the + * user; an empty string for the user is equivalent to removing user + * information. + * + * @param string $user The user name to use for authority. + * @param null|string $password The password associated with $user. + * @return static A new instance with the specified user information. + */ + public function withUserInfo(string $user, ?string $password = null); + + /** + * Return an instance with the specified host. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified host. + * + * An empty host value is equivalent to removing the host. + * + * @param string $host The hostname to use with the new instance. + * @return static A new instance with the specified host. + * @throws \InvalidArgumentException for invalid hostnames. + */ + public function withHost(string $host); + + /** + * Return an instance with the specified port. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified port. + * + * Implementations MUST raise an exception for ports outside the + * established TCP and UDP port ranges. + * + * A null value provided for the port is equivalent to removing the port + * information. + * + * @param null|int $port The port to use with the new instance; a null value + * removes the port information. + * @return static A new instance with the specified port. + * @throws \InvalidArgumentException for invalid ports. + */ + public function withPort(?int $port); + + /** + * Return an instance with the specified path. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified path. + * + * The path can either be empty or absolute (starting with a slash) or + * rootless (not starting with a slash). Implementations MUST support all + * three syntaxes. + * + * If the path is intended to be domain-relative rather than path relative then + * it must begin with a slash ("/"). Paths not starting with a slash ("/") + * are assumed to be relative to some base path known to the application or + * consumer. + * + * Users can provide both encoded and decoded path characters. + * Implementations ensure the correct encoding as outlined in getPath(). + * + * @param string $path The path to use with the new instance. + * @return static A new instance with the specified path. + * @throws \InvalidArgumentException for invalid paths. + */ + public function withPath(string $path); + + /** + * Return an instance with the specified query string. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified query string. + * + * Users can provide both encoded and decoded query characters. + * Implementations ensure the correct encoding as outlined in getQuery(). + * + * An empty query string value is equivalent to removing the query string. + * + * @param string $query The query string to use with the new instance. + * @return static A new instance with the specified query string. + * @throws \InvalidArgumentException for invalid query strings. + */ + public function withQuery(string $query); + + /** + * Return an instance with the specified URI fragment. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified URI fragment. + * + * Users can provide both encoded and decoded fragment characters. + * Implementations ensure the correct encoding as outlined in getFragment(). + * + * An empty fragment value is equivalent to removing the fragment. + * + * @param string $fragment The fragment to use with the new instance. + * @return static A new instance with the specified fragment. + */ + public function withFragment(string $fragment); + + /** + * Return the string representation as a URI reference. + * + * Depending on which components of the URI are present, the resulting + * string is either a full URI or relative reference according to RFC 3986, + * Section 4.1. The method concatenates the various components of the URI, + * using the appropriate delimiters: + * + * - If a scheme is present, it MUST be suffixed by ":". + * - If an authority is present, it MUST be prefixed by "//". + * - The path can be concatenated without delimiters. But there are two + * cases where the path has to be adjusted to make the URI reference + * valid as PHP does not allow to throw an exception in __toString(): + * - If the path is rootless and an authority is present, the path MUST + * be prefixed by "/". + * - If the path is starting with more than one "/" and no authority is + * present, the starting slashes MUST be reduced to one. + * - If a query is present, it MUST be prefixed by "?". + * - If a fragment is present, it MUST be prefixed by "#". + * + * @see http://tools.ietf.org/html/rfc3986#section-4.1 + * @return string + */ + public function __toString(); +} diff --git a/vendor/psr/log/LICENSE b/vendor/psr/log/LICENSE new file mode 100644 index 0000000..474c952 --- /dev/null +++ b/vendor/psr/log/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2012 PHP Framework Interoperability Group + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/psr/log/README.md b/vendor/psr/log/README.md new file mode 100644 index 0000000..a9f20c4 --- /dev/null +++ b/vendor/psr/log/README.md @@ -0,0 +1,58 @@ +PSR Log +======= + +This repository holds all interfaces/classes/traits related to +[PSR-3](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md). + +Note that this is not a logger of its own. It is merely an interface that +describes a logger. See the specification for more details. + +Installation +------------ + +```bash +composer require psr/log +``` + +Usage +----- + +If you need a logger, you can use the interface like this: + +```php +logger = $logger; + } + + public function doSomething() + { + if ($this->logger) { + $this->logger->info('Doing work'); + } + + try { + $this->doSomethingElse(); + } catch (Exception $exception) { + $this->logger->error('Oh no!', array('exception' => $exception)); + } + + // do something useful + } +} +``` + +You can then pick one of the implementations of the interface to get a logger. + +If you want to implement the interface, you can require this package and +implement `Psr\Log\LoggerInterface` in your code. Please read the +[specification text](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md) +for details. diff --git a/vendor/psr/log/composer.json b/vendor/psr/log/composer.json new file mode 100644 index 0000000..879fc6f --- /dev/null +++ b/vendor/psr/log/composer.json @@ -0,0 +1,26 @@ +{ + "name": "psr/log", + "description": "Common interface for logging libraries", + "keywords": ["psr", "psr-3", "log"], + "homepage": "https://github.com/php-fig/log", + "license": "MIT", + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "require": { + "php": ">=8.0.0" + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + } +} diff --git a/vendor/psr/log/src/AbstractLogger.php b/vendor/psr/log/src/AbstractLogger.php new file mode 100644 index 0000000..d60a091 --- /dev/null +++ b/vendor/psr/log/src/AbstractLogger.php @@ -0,0 +1,15 @@ +logger = $logger; + } +} diff --git a/vendor/psr/log/src/LoggerInterface.php b/vendor/psr/log/src/LoggerInterface.php new file mode 100644 index 0000000..cb4cf64 --- /dev/null +++ b/vendor/psr/log/src/LoggerInterface.php @@ -0,0 +1,98 @@ +log(LogLevel::EMERGENCY, $message, $context); + } + + /** + * Action must be taken immediately. + * + * Example: Entire website down, database unavailable, etc. This should + * trigger the SMS alerts and wake you up. + */ + public function alert(string|\Stringable $message, array $context = []): void + { + $this->log(LogLevel::ALERT, $message, $context); + } + + /** + * Critical conditions. + * + * Example: Application component unavailable, unexpected exception. + */ + public function critical(string|\Stringable $message, array $context = []): void + { + $this->log(LogLevel::CRITICAL, $message, $context); + } + + /** + * Runtime errors that do not require immediate action but should typically + * be logged and monitored. + */ + public function error(string|\Stringable $message, array $context = []): void + { + $this->log(LogLevel::ERROR, $message, $context); + } + + /** + * Exceptional occurrences that are not errors. + * + * Example: Use of deprecated APIs, poor use of an API, undesirable things + * that are not necessarily wrong. + */ + public function warning(string|\Stringable $message, array $context = []): void + { + $this->log(LogLevel::WARNING, $message, $context); + } + + /** + * Normal but significant events. + */ + public function notice(string|\Stringable $message, array $context = []): void + { + $this->log(LogLevel::NOTICE, $message, $context); + } + + /** + * Interesting events. + * + * Example: User logs in, SQL logs. + */ + public function info(string|\Stringable $message, array $context = []): void + { + $this->log(LogLevel::INFO, $message, $context); + } + + /** + * Detailed debug information. + */ + public function debug(string|\Stringable $message, array $context = []): void + { + $this->log(LogLevel::DEBUG, $message, $context); + } + + /** + * Logs with an arbitrary level. + * + * @param mixed $level + * + * @throws \Psr\Log\InvalidArgumentException + */ + abstract public function log($level, string|\Stringable $message, array $context = []): void; +} diff --git a/vendor/psr/log/src/NullLogger.php b/vendor/psr/log/src/NullLogger.php new file mode 100644 index 0000000..de0561e --- /dev/null +++ b/vendor/psr/log/src/NullLogger.php @@ -0,0 +1,26 @@ +logger) { }` + * blocks. + */ +class NullLogger extends AbstractLogger +{ + /** + * Logs with an arbitrary level. + * + * @param mixed[] $context + * + * @throws \Psr\Log\InvalidArgumentException + */ + public function log($level, string|\Stringable $message, array $context = []): void + { + // noop + } +} diff --git a/vendor/textalk/websocket/.github/ISSUE_TEMPLATE/bug_report.md b/vendor/textalk/websocket/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..d402046 --- /dev/null +++ b/vendor/textalk/websocket/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,21 @@ +--- +name: Bug report +about: Use this if you believe there is a bug in this repo +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** +Please provide a clear and concise description of the suspected issue. + +**How to reproduce** +If possible, provide information - possibly including code snippets - on how to reproduce the issue. + +**Logs** +If possible, provide logs that indicate the issue. See https://github.com/Textalk/websocket-php/blob/master/docs/Examples.md#echo-logger on how to use the EchoLog. + +**Versions** +* Version of this library +* PHP version diff --git a/vendor/textalk/websocket/.github/ISSUE_TEMPLATE/feature_request.md b/vendor/textalk/websocket/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..ce777f6 --- /dev/null +++ b/vendor/textalk/websocket/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,14 @@ +--- +name: Feature request +about: Suggest an idea for this library +title: '' +labels: feature request +assignees: '' + +--- + +**Is it within the scope of this library?** +Consider and describe why the feature would be beneficial in this library, and not implemented as a separate project using this as a dependency. + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. diff --git a/vendor/textalk/websocket/.github/ISSUE_TEMPLATE/other-issue.md b/vendor/textalk/websocket/.github/ISSUE_TEMPLATE/other-issue.md new file mode 100644 index 0000000..fe5cc8d --- /dev/null +++ b/vendor/textalk/websocket/.github/ISSUE_TEMPLATE/other-issue.md @@ -0,0 +1,10 @@ +--- +name: Other issue +about: Use this for other issues +title: '' +labels: '' +assignees: '' + +--- + +**Describe your issue** diff --git a/vendor/textalk/websocket/.github/workflows/acceptance.yml b/vendor/textalk/websocket/.github/workflows/acceptance.yml new file mode 100644 index 0000000..a55f2a7 --- /dev/null +++ b/vendor/textalk/websocket/.github/workflows/acceptance.yml @@ -0,0 +1,97 @@ +name: Acceptance + +on: [push, pull_request] + +jobs: + test-7-4: + runs-on: ubuntu-latest + name: Test PHP 7.4 + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Set up PHP 7.4 + uses: shivammathur/setup-php@v2 + with: + php-version: '7.4' + - name: Composer + run: make install + - name: Test + run: make test + + test-8-0: + runs-on: ubuntu-latest + name: Test PHP 8.0 + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Set up PHP 8.0 + uses: shivammathur/setup-php@v2 + with: + php-version: '8.0' + - name: Composer + run: make install + - name: Test + run: make test + + test-8-1: + runs-on: ubuntu-latest + name: Test PHP 8.1 + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Set up PHP 8.1 + uses: shivammathur/setup-php@v2 + with: + php-version: '8.1' + - name: Composer + run: make install + - name: Test + run: make test + + test-8-2: + runs-on: ubuntu-latest + name: Test PHP 8.2 + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Set up PHP 8.2 + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + - name: Composer + run: make install + - name: Test + run: make test + + cs-check: + runs-on: ubuntu-latest + name: Code standard + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Set up PHP 8.0 + uses: shivammathur/setup-php@v2 + with: + php-version: '8.0' + - name: Composer + run: make install + - name: Code standard + run: make cs-check + + coverage: + runs-on: ubuntu-latest + name: Code coverage + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Set up PHP 8.0 + uses: shivammathur/setup-php@v2 + with: + php-version: '8.0' + extensions: xdebug + - name: Composer + run: make install + - name: Code coverage + env: + COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: make coverage diff --git a/vendor/textalk/websocket/.gitignore b/vendor/textalk/websocket/.gitignore new file mode 100644 index 0000000..379ab4b --- /dev/null +++ b/vendor/textalk/websocket/.gitignore @@ -0,0 +1,6 @@ +.DS_Store +.phpunit.result.cache +build/ +composer.lock +composer.phar +vendor/ \ No newline at end of file diff --git a/vendor/textalk/websocket/COPYING.md b/vendor/textalk/websocket/COPYING.md new file mode 100644 index 0000000..ba96480 --- /dev/null +++ b/vendor/textalk/websocket/COPYING.md @@ -0,0 +1,16 @@ +# Websocket: License + +Websocket PHP is free software released under the following license: + +[ISC License](http://en.wikipedia.org/wiki/ISC_license) + +Permission to use, copy, modify, and/or distribute this software for any purpose with or without +fee is hereby granted, provided that the above copyright notice and this permission notice appear +in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS +SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. diff --git a/vendor/textalk/websocket/Makefile b/vendor/textalk/websocket/Makefile new file mode 100644 index 0000000..54d507e --- /dev/null +++ b/vendor/textalk/websocket/Makefile @@ -0,0 +1,32 @@ +install: composer.phar + ./composer.phar install + +update: composer.phar + ./composer.phar self-update + ./composer.phar update + +test: composer.lock + ./vendor/bin/phpunit + +cs-check: composer.lock + ./vendor/bin/phpcs --standard=PSR1,PSR12 --encoding=UTF-8 --report=full --colors lib tests examples + +coverage: composer.lock build + XDEBUG_MODE=coverage ./vendor/bin/phpunit --coverage-clover build/logs/clover.xml + ./vendor/bin/php-coveralls -v + +composer.phar: + curl -s http://getcomposer.org/installer | php + +composer.lock: composer.phar + ./composer.phar --no-interaction install + +vendor/bin/phpunit: install + +build: + mkdir build + +clean: + rm composer.phar + rm -r vendor + rm -r build diff --git a/vendor/textalk/websocket/README.md b/vendor/textalk/websocket/README.md new file mode 100644 index 0000000..c787131 --- /dev/null +++ b/vendor/textalk/websocket/README.md @@ -0,0 +1,76 @@ +# Websocket Client and Server for PHP + +[![Build Status](https://github.com/Textalk/websocket-php/actions/workflows/acceptance.yml/badge.svg)](https://github.com/Textalk/websocket-php/actions) +[![Coverage Status](https://coveralls.io/repos/github/Textalk/websocket-php/badge.svg?branch=master)](https://coveralls.io/github/Textalk/websocket-php) + +## Archived project + +This project has been archived and is no longer maintained. No bug fix and no additional features will be added.
+You won't be able to submit new issues or pull requests, and no additional features will be added + +This library has been replaced by [sirn-se/websocket-php](https://github.com/sirn-se/websocket-php) + +## Websocket Client and Server for PHP + +This library contains WebSocket client and server for PHP. + +The client and server provides methods for reading and writing to WebSocket streams. +It does not include convenience operations such as listeners and implicit error handling. + +## Documentation + +- [Client](docs/Client.md) +- [Server](docs/Server.md) +- [Examples](docs/Examples.md) +- [Changelog](docs/Changelog.md) +- [Contributing](docs/Contributing.md) + +## Installing + +Preferred way to install is with [Composer](https://getcomposer.org/). +``` +composer require textalk/websocket +``` + +* Current version support PHP versions `^7.4|^8.0`. +* For PHP `7.2` and `7.3` support use version [`1.5`](https://github.com/Textalk/websocket-php/tree/1.5.0). +* For PHP `7.1` support use version [`1.4`](https://github.com/Textalk/websocket-php/tree/1.4.0). +* For PHP `^5.4` and `7.0` support use version [`1.3`](https://github.com/Textalk/websocket-php/tree/1.3.0). + +## Client + +The [client](docs/Client.md) can read and write on a WebSocket stream. +It internally supports Upgrade handshake and implicit close and ping/pong operations. + +```php +$client = new WebSocket\Client("ws://echo.websocket.org/"); +$client->text("Hello WebSocket.org!"); +echo $client->receive(); +$client->close(); +``` + +## Server + +The library contains a rudimentary single stream/single thread [server](docs/Server.md). +It internally supports Upgrade handshake and implicit close and ping/pong operations. + +Note that it does **not** support threading or automatic association ot continuous client requests. +If you require this kind of server behavior, you need to build it on top of provided server implementation. + +```php +$server = new WebSocket\Server(); +$server->accept(); +$message = $server->receive(); +$server->text($message); +$server->close(); +``` + +### License and Contributors + +[ISC License](COPYING.md) + +Fredrik Liljegren, Armen Baghumian Sankbarani, Ruslan Bekenev, +Joshua Thijssen, Simon Lipp, Quentin Bellus, Patrick McCarren, swmcdonnell, +Ignas Bernotas, Mark Herhold, Andreas Palm, Sören Jensen, pmaasz, Alexey Stavrov, +Michael Slezak, Pierre Seznec, rmeisler, Nickolay V. Shmyrev, Christoph Kempen, +Marc Roberts, Antonio Mora, Simon Podlipsky, etrinh. diff --git a/vendor/textalk/websocket/composer.json b/vendor/textalk/websocket/composer.json new file mode 100644 index 0000000..23018ee --- /dev/null +++ b/vendor/textalk/websocket/composer.json @@ -0,0 +1,36 @@ +{ + "name": "textalk/websocket", + "description": "WebSocket client and server", + "license": "ISC", + "type": "library", + "authors": [ + { + "name": "Fredrik Liljegren" + }, + { + "name": "Sören Jensen" + } + ], + "autoload": { + "psr-4": { + "WebSocket\\": "lib" + } + }, + "autoload-dev": { + "psr-4": { + "WebSocket\\": "tests/mock" + } + }, + "require": { + "php": "^7.4 | ^8.0", + "phrity/net-uri": "^1.0", + "phrity/util-errorhandler": "^1.0", + "psr/log": "^1.0 | ^2.0 | ^3.0", + "psr/http-message": "^1.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0", + "php-coveralls/php-coveralls": "^2.0", + "squizlabs/php_codesniffer": "^3.5" + } +} diff --git a/vendor/textalk/websocket/docs/Changelog.md b/vendor/textalk/websocket/docs/Changelog.md new file mode 100644 index 0000000..38f264e --- /dev/null +++ b/vendor/textalk/websocket/docs/Changelog.md @@ -0,0 +1,167 @@ +[Client](Client.md) • [Server](Server.md) • [Message](Message.md) • [Examples](Examples.md) • Changelog • [Contributing](Contributing.md) + +# Websocket: Changelog + +## `v1.6` + + > PHP version `^7.4|^8.0` + +### `1.6.3` + + * Fix issue with implicit default ports (@etrinh, @sirn-se) + +### `1.6.2` + + * Fix issue where port was missing in socket uri (@sirn-se) + +### `1.6.1` + + * Fix client path for http request (@simPod, @sirn-se) + +### `1.6.0` + * Connection separate from Client and Server (@sirn-se) + * getPier() deprecated, replaced by getRemoteName() (@sirn-se) + * Client accepts `Psr\Http\Message\UriInterface` as input for URI:s (@sirn-se) + * Bad URI throws exception when Client is instanciated, previously when used (@sirn-se) + * Preparations for multiple conection and listeners (@sirn-se) + * Major internal refactoring (@sirn-se) + +## `v1.5` + + > PHP version `^7.2|^8.0` + +### `1.5.8` + + * Handle read error during handshake (@sirn-se) + +### `1.5.7` + + * Large header block fix (@sirn-se) + +### `1.5.6` + + * Add test for PHP 8.1 (@sirn-se) + * Code standard (@sirn-se) + +### `1.5.5` + + * Support for psr/log v2 and v3 (@simPod) + * GitHub Actions replaces Travis (@sirn-se) + +### `1.5.4` + + * Keep open connection on read timeout (@marcroberts) + +### `1.5.3` + + * Fix for persistent connection (@sirn-se) + +### `1.5.2` + + * Fix for getName() method (@sirn-se) + +### `1.5.1` + + * Fix for persistent connections (@rmeisler) + +### `1.5.0` + + * Convenience send methods; text(), binary(), ping(), pong() (@sirn-se) + * Optional Message instance as receive() method return (@sirn-se) + * Opcode filter for receive() method (@sirn-se) + * Added PHP `8.0` support (@webpatser) + * Dropped PHP `7.1` support (@sirn-se) + * Fix for unordered fragmented messages (@sirn-se) + * Improved error handling on stream calls (@sirn-se) + * Various code re-write (@sirn-se) + +## `v1.4` + + > PHP version `^7.1` + +#### `1.4.3` + + * Solve stream closure/get meta conflict (@sirn-se) + * Examples and documentation overhaul (@sirn-se) + +#### `1.4.2` + + * Force stream close on read error (@sirn-se) + * Authorization headers line feed (@sirn-se) + * Documentation (@matias-pool, @sirn-se) + +#### `1.4.1` + + * Ping/Pong, handled internally to avoid breaking fragmented messages (@nshmyrev, @sirn-se) + * Fix for persistent connections (@rmeisler) + * Fix opcode bitmask (@peterjah) + +#### `1.4.0` + + * Dropped support of old PHP versions (@sirn-se) + * Added PSR-3 Logging support (@sirn-se) + * Persistent connection option (@slezakattack) + * TimeoutException on connection time out (@slezakattack) + +## `v1.3` + + > PHP version `^5.4` and `^7.0` + +#### `1.3.1` + + * Allow control messages without payload (@Logioniz) + * Error code in ConnectionException (@sirn-se) + +#### `1.3.0` + + * Implements ping/pong frames (@pmccarren @Logioniz) + * Close behaviour (@sirn-se) + * Various fixes concerning connection handling (@sirn-se) + * Overhaul of Composer, Travis and Coveralls setup, PSR code standard and unit tests (@sirn-se) + +## `v1.2` + + > PHP version `^5.4` and `^7.0` + +#### `1.2.0` + + * Adding stream context options (to set e.g. SSL `allow_self_signed`). + +## `v1.1` + + > PHP version `^5.4` and `^7.0` + +#### `1.1.2` + + * Fixed error message on broken frame. + +#### `1.1.1` + + * Adding license information. + +#### `1.1.0` + + * Supporting huge payloads. + +## `v1.0` + + > PHP version `^5.4` and `^7.0` + +#### `1.0.3` + + * Bugfix: Correcting address in error-message + +#### `1.0.2` + + * Bugfix: Add port in request-header. + +#### `1.0.1` + + * Fixing a bug from empty payloads. + +#### `1.0.0` + + * Release as production ready. + * Adding option to set/override headers. + * Supporting basic authentication from user:pass in URL. + diff --git a/vendor/textalk/websocket/docs/Client.md b/vendor/textalk/websocket/docs/Client.md new file mode 100644 index 0000000..be8d285 --- /dev/null +++ b/vendor/textalk/websocket/docs/Client.md @@ -0,0 +1,137 @@ +Client • [Server](Server.md) • [Message](Message.md) • [Examples](Examples.md) • [Changelog](Changelog.md) • [Contributing](Contributing.md) + +# Websocket: Client + +The client can read and write on a WebSocket stream. +It internally supports Upgrade handshake and implicit close and ping/pong operations. + +## Class synopsis + +```php +WebSocket\Client { + + public __construct(UriInterface|string $uri, array $options = []); + public __destruct(); + public __toString() : string; + + public text(string $payload) : void; + public binary(string $payload) : void; + public ping(string $payload = '') : void; + public pong(string $payload = '') : void; + public send(Message|string $payload, string $opcode = 'text', bool $masked = true) : void; + public close(int $status = 1000, mixed $message = 'ttfn') : void; + public receive() : Message|string|null; + + public getName() : string|null; + public getRemoteName() : string|null; + public getLastOpcode() : string; + public getCloseStatus() : int; + public isConnected() : bool; + public setTimeout(int $seconds) : void; + public setFragmentSize(int $fragment_size) : self; + public getFragmentSize() : int; + public setLogger(Psr\Log\LoggerInterface $logger = null) : void; +} +``` + +## Examples + +### Simple send-receive operation + +This example send a single message to a server, and output the response. + +```php +$client = new WebSocket\Client("ws://echo.websocket.org/"); +$client->text("Hello WebSocket.org!"); +echo $client->receive(); +$client->close(); +``` + +### Listening to a server + +To continuously listen to incoming messages, you need to put the receive operation within a loop. +Note that these functions **always** throw exception on any failure, including recoverable failures such as connection time out. +By consuming exceptions, the code will re-connect the socket in next loop iteration. + +```php +$client = new WebSocket\Client("ws://echo.websocket.org/"); +while (true) { + try { + $message = $client->receive(); + // Act on received message + // Break while loop to stop listening + } catch (\WebSocket\ConnectionException $e) { + // Possibly log errors + } +} +$client->close(); +``` + +### Filtering received messages + +By default the `receive()` method return messages of 'text' and 'binary' opcode. +The filter option allows you to specify which message types to return. + +```php +$client = new WebSocket\Client("ws://echo.websocket.org/", ['filter' => ['text']]); +$client->receive(); // Only return 'text' messages + +$client = new WebSocket\Client("ws://echo.websocket.org/", ['filter' => ['text', 'binary', 'ping', 'pong', 'close']]); +$client->receive(); // Return all messages +``` + +### Sending messages + +There are convenience methods to send messages with different opcodes. +```php +$client = new WebSocket\Client("ws://echo.websocket.org/"); + +// Convenience methods +$client->text('A plain text message'); // Send an opcode=text message +$client->binary($binary_string); // Send an opcode=binary message +$client->ping(); // Send an opcode=ping frame +$client->pong(); // Send an unsolicited opcode=pong frame + +// Generic send method +$client->send($payload); // Sent as masked opcode=text +$client->send($payload, 'binary'); // Sent as masked opcode=binary +$client->send($payload, 'binary', false); // Sent as unmasked opcode=binary +``` + +## Constructor options + +The `$options` parameter in constructor accepts an associative array of options. + +* `context` - A stream context created using [stream_context_create](https://www.php.net/manual/en/function.stream-context-create). +* `filter` - Array of opcodes to return on receive, default `['text', 'binary']` +* `fragment_size` - Maximum payload size. Default 4096 chars. +* `headers` - Additional headers as associative array name => content. +* `logger` - A [PSR-3](https://www.php-fig.org/psr/psr-3/) compatible logger. +* `persistent` - Connection is re-used between requests until time out is reached. Default false. +* `return_obj` - Return a [Message](Message.md) instance on receive, default false +* `timeout` - Time out in seconds. Default 5 seconds. + +```php +$context = stream_context_create(); +stream_context_set_option($context, 'ssl', 'verify_peer', false); +stream_context_set_option($context, 'ssl', 'verify_peer_name', false); + +$client = new WebSocket\Client("ws://echo.websocket.org/", [ + 'context' => $context, // Attach stream context created above + 'filter' => ['text', 'binary', 'ping'], // Specify message types for receive() to return + 'headers' => [ // Additional headers, used to specify subprotocol + 'Sec-WebSocket-Protocol' => 'soap', + 'origin' => 'localhost', + ], + 'logger' => $my_psr3_logger, // Attach a PSR3 compatible logger + 'return_obj' => true, // Return Message instance rather than just text + 'timeout' => 60, // 1 minute time out +]); +``` + +## Exceptions + +* `WebSocket\BadOpcodeException` - Thrown if provided opcode is invalid. +* `WebSocket\BadUriException` - Thrown if provided URI is invalid. +* `WebSocket\ConnectionException` - Thrown on any socket I/O failure. +* `WebSocket\TimeoutException` - Thrown when the socket experiences a time out. diff --git a/vendor/textalk/websocket/docs/Contributing.md b/vendor/textalk/websocket/docs/Contributing.md new file mode 100644 index 0000000..c68ab83 --- /dev/null +++ b/vendor/textalk/websocket/docs/Contributing.md @@ -0,0 +1,51 @@ +[Client](Client.md) • [Server](Server.md) • [Message](Message.md) • [Examples](Examples.md) • [Changelog](Changelog.md) • Contributing + +# Websocket: Contributing + +Everyone is welcome to help out! +But to keep this project sustainable, please ensure your contribution respects the requirements below. + +## PR Requirements + +Requirements on pull requests; +* All tests **MUST** pass. +* Code coverage **MUST** remain at 100%. +* Code **MUST** adhere to PSR-1 and PSR-12 code standards. + +Base your patch on corresponding version branch, and target that version branch in your pull request. + +* `v1.6-master` current version +* `v1.5-master` previous version, bug fixes only +* Older versions should not be target of pull requests + + +## Dependency management + +Install or update dependencies using [Composer](https://getcomposer.org/). + +``` +# Install dependencies +make install + +# Update dependencies +make update +``` + +## Code standard + +This project uses [PSR-1](https://www.php-fig.org/psr/psr-1/) and [PSR-12](https://www.php-fig.org/psr/psr-12/) code standards. +``` +# Check code standard adherence +make cs-check +``` + +## Unit testing + +Unit tests with [PHPUnit](https://phpunit.readthedocs.io/), coverage with [Coveralls](https://github.com/php-coveralls/php-coveralls) +``` +# Run unit tests +make test + +# Create coverage +make coverage +``` diff --git a/vendor/textalk/websocket/docs/Examples.md b/vendor/textalk/websocket/docs/Examples.md new file mode 100644 index 0000000..399e0cc --- /dev/null +++ b/vendor/textalk/websocket/docs/Examples.md @@ -0,0 +1,101 @@ +[Client](Client.md) • [Server](Server.md) • [Message](Message.md) • Examples • [Changelog](Changelog.md) • [Contributing](Contributing.md) + +# Websocket: Examples + +Here are some examples on how to use the WebSocket library. + +## Echo logger + +In dev environment (as in having run composer to include dev dependencies) you have +access to a simple echo logger that print out information synchronously. + +This is usable for debugging. For production, use a proper logger. + +```php +namespace WebSocket; + +$logger = new EchoLogger(); + +$client = new Client('ws://echo.websocket.org/'); +$client->setLogger($logger); + +$server = new Server(); +$server->setLogger($logger); +``` + +An example of server output; +``` +info | Server listening to port 8000 [] +debug | Wrote 129 of 129 bytes. [] +info | Server connected to port 8000 [] +info | Received 'text' message [] +debug | Wrote 9 of 9 bytes. [] +info | Sent 'text' message [] +debug | Received 'close', status: 1000. [] +debug | Wrote 32 of 32 bytes. [] +info | Sent 'close' message [] +info | Received 'close' message [] +``` + +## The `send` client + +Source: [examples/send.php](../examples/send.php) + +A simple, single send/receive client. + +Example use: +``` +php examples/send.php --opcode text "A text message" // Send a text message to localhost +php examples/send.php --opcode ping "ping it" // Send a ping message to localhost +php examples/send.php --uri ws://echo.websocket.org "A text message" // Send a text message to echo.websocket.org +php examples/send.php --opcode text --debug "A text message" // Use runtime debugging +``` + +## The `echoserver` server + +Source: [examples/echoserver.php](../examples/echoserver.php) + +A simple server that responds to recevied commands. + +Example use: +``` +php examples/echoserver.php // Run with default settings +php examples/echoserver.php --port 8080 // Listen on port 8080 +php examples/echoserver.php --debug // Use runtime debugging +``` + +These strings can be sent as message to trigger server to perform actions; +* `auth` - Server will respond with auth header if provided by client +* `close` - Server will close current connection +* `exit` - Server will close all active connections +* `headers` - Server will respond with all headers provided by client +* `ping` - Server will send a ping message +* `pong` - Server will send a pong message +* `stop` - Server will stop listening +* For other sent strings, server will respond with the same strings + +## The `random` client + +Source: [examples/random_client.php](../examples/random_client.php) + +The random client will use random options and continuously send/receive random messages. + +Example use: +``` +php examples/random_client.php --uri ws://echo.websocket.org // Connect to echo.websocket.org +php examples/random_client.php --timeout 5 --fragment_size 16 // Specify settings +php examples/random_client.php --debug // Use runtime debugging +``` + +## The `random` server + +Source: [examples/random_server.php](../examples/random_server.php) + +The random server will use random options and continuously send/receive random messages. + +Example use: +``` +php examples/random_server.php --port 8080 // // Listen on port 8080 +php examples/random_server.php --timeout 5 --fragment_size 16 // Specify settings +php examples/random_server.php --debug // Use runtime debugging +``` diff --git a/vendor/textalk/websocket/docs/Message.md b/vendor/textalk/websocket/docs/Message.md new file mode 100644 index 0000000..80df04a --- /dev/null +++ b/vendor/textalk/websocket/docs/Message.md @@ -0,0 +1,60 @@ +[Client](Client.md) • [Server](Server.md) • Message • [Examples](Examples.md) • [Changelog](Changelog.md) • [Contributing](Contributing.md) + +# Websocket: Messages + +If option `return_obj` is set to `true` on [client](Client.md) or [server](Server.md), +the `receive()` method will return a Message instance instead of a string. + +Available classes correspond to opcode; +* WebSocket\Message\Text +* WebSocket\Message\Binary +* WebSocket\Message\Ping +* WebSocket\Message\Pong +* WebSocket\Message\Close + +Additionally; +* WebSocket\Message\Message - abstract base class for all messages above +* WebSocket\Message\Factory - Factory class to create Message instances + +## Message abstract class synopsis + +```php +WebSocket\Message\Message { + + public __construct(string $payload = ''); + public __toString() : string; + + public getOpcode() : string; + public getLength() : int; + public getTimestamp() : DateTime; + public getContent() : string; + public setContent(string $payload = '') : void; + public hasContent() : bool; +} +``` + +## Factory class synopsis + +```php +WebSocket\Message\Factory { + + public create(string $opcode, string $payload = '') : Message; +} +``` + +## Example + +Receving a Message and echo some methods. + +```php +$client = new WebSocket\Client('ws://echo.websocket.org/', ['return_obj' => true]); +$client->text('Hello WebSocket.org!'); +// Echo return same message as sent +$message = $client->receive(); +echo $message->getOpcode(); // -> "text" +echo $message->getLength(); // -> 20 +echo $message->getContent(); // -> "Hello WebSocket.org!" +echo $message->hasContent(); // -> true +echo $message->getTimestamp()->format('H:i:s'); // -> 19:37:18 +$client->close(); +``` diff --git a/vendor/textalk/websocket/docs/Server.md b/vendor/textalk/websocket/docs/Server.md new file mode 100644 index 0000000..9e12e07 --- /dev/null +++ b/vendor/textalk/websocket/docs/Server.md @@ -0,0 +1,136 @@ +[Client](Client.md) • Server • [Message](Message.md) • [Examples](Examples.md) • [Changelog](Changelog.md) • [Contributing](Contributing.md) + +# Websocket: Server + +The library contains a rudimentary single stream/single thread server. +It internally supports Upgrade handshake and implicit close and ping/pong operations. + +Note that it does **not** support threading or automatic association ot continuous client requests. +If you require this kind of server behavior, you need to build it on top of provided server implementation. + +## Class synopsis + +```php +WebSocket\Server { + + public __construct(array $options = []); + public __destruct(); + public __toString() : string; + + public accept() : bool; + public text(string $payload) : void; + public binary(string $payload) : void; + public ping(string $payload = '') : void; + public pong(string $payload = '') : void; + public send(Message|string $payload, string $opcode = 'text', bool $masked = true) : void; + public close(int $status = 1000, mixed $message = 'ttfn') : void; + public receive() : Message|string|null; + + public getPort() : int; + public getPath() : string; + public getRequest() : array; + public getHeader(string $header_name) : string|null; + + public getName() : string|null; + public getRemoteName() : string|null; + public getLastOpcode() : string; + public getCloseStatus() : int; + public isConnected() : bool; + public setTimeout(int $seconds) : void; + public setFragmentSize(int $fragment_size) : self; + public getFragmentSize() : int; + public setLogger(Psr\Log\LoggerInterface $logger = null) : void; +} +``` + +## Examples + +### Simple receive-send operation + +This example reads a single message from a client, and respond with the same message. + +```php +$server = new WebSocket\Server(); +$server->accept(); +$message = $server->receive(); +$server->text($message); +$server->close(); +``` + +### Listening to clients + +To continuously listen to incoming messages, you need to put the receive operation within a loop. +Note that these functions **always** throw exception on any failure, including recoverable failures such as connection time out. +By consuming exceptions, the code will re-connect the socket in next loop iteration. + +```php +$server = new WebSocket\Server(); +while ($server->accept()) { + try { + $message = $server->receive(); + // Act on received message + // Break while loop to stop listening + } catch (\WebSocket\ConnectionException $e) { + // Possibly log errors + } +} +$server->close(); +``` + +### Filtering received messages + +By default the `receive()` method return messages of 'text' and 'binary' opcode. +The filter option allows you to specify which message types to return. + +```php +$server = new WebSocket\Server(['filter' => ['text']]); +$server->receive(); // only return 'text' messages + +$server = new WebSocket\Server(['filter' => ['text', 'binary', 'ping', 'pong', 'close']]); +$server->receive(); // return all messages +``` + +### Sending messages + +There are convenience methods to send messages with different opcodes. +```php +$server = new WebSocket\Server(); + +// Convenience methods +$server->text('A plain text message'); // Send an opcode=text message +$server->binary($binary_string); // Send an opcode=binary message +$server->ping(); // Send an opcode=ping frame +$server->pong(); // Send an unsolicited opcode=pong frame + +// Generic send method +$server->send($payload); // Sent as masked opcode=text +$server->send($payload, 'binary'); // Sent as masked opcode=binary +$server->send($payload, 'binary', false); // Sent as unmasked opcode=binary +``` + +## Constructor options + +The `$options` parameter in constructor accepts an associative array of options. + +* `filter` - Array of opcodes to return on receive, default `['text', 'binary']` +* `fragment_size` - Maximum payload size. Default 4096 chars. +* `logger` - A [PSR-3](https://www.php-fig.org/psr/psr-3/) compatible logger. +* `port` - The server port to listen to. Default 8000. +* `return_obj` - Return a [Message](Message.md) instance on receive, default false +* `timeout` - Time out in seconds. Default 5 seconds. + +```php +$server = new WebSocket\Server([ + 'filter' => ['text', 'binary', 'ping'], // Specify message types for receive() to return + 'logger' => $my_psr3_logger, // Attach a PSR3 compatible logger + 'port' => 9000, // Listening port + 'return_obj' => true, // Return Message instance rather than just text + 'timeout' => 60, // 1 minute time out +]); +``` + +## Exceptions + +* `WebSocket\BadOpcodeException` - Thrown if provided opcode is invalid. +* `WebSocket\ConnectionException` - Thrown on any socket I/O failure. +* `WebSocket\TimeoutException` - Thrown when the socket experiences a time out. diff --git a/vendor/textalk/websocket/examples/echoserver.php b/vendor/textalk/websocket/examples/echoserver.php new file mode 100644 index 0000000..a85e564 --- /dev/null +++ b/vendor/textalk/websocket/examples/echoserver.php @@ -0,0 +1,87 @@ + : The port to listen to, default 8000 + * --timeout : Timeout in seconds, default 200 seconds + * --debug : Output log data (if logger is available) + */ + +namespace WebSocket; + +require __DIR__ . '/../vendor/autoload.php'; + +error_reporting(-1); + +echo "> Echo server\n"; + +// Server options specified or random +$options = array_merge([ + 'port' => 8000, + 'timeout' => 200, + 'filter' => ['text', 'binary', 'ping', 'pong', 'close'], +], getopt('', ['port:', 'timeout:', 'debug'])); + +// If debug mode and logger is available +if (isset($options['debug']) && class_exists('WebSocket\EchoLog')) { + $logger = new EchoLog(); + $options['logger'] = $logger; + echo "> Using logger\n"; +} + +// Initiate server. +try { + $server = new Server($options); +} catch (ConnectionException $e) { + echo "> ERROR: {$e->getMessage()}\n"; + die(); +} + +echo "> Listening to port {$server->getPort()}\n"; + +// Force quit to close server +while (true) { + try { + while ($server->accept()) { + echo "> Accepted on port {$server->getPort()}\n"; + while (true) { + $message = $server->receive(); + $opcode = $server->getLastOpcode(); + if (is_null($message)) { + echo "> Closing connection\n"; + continue 2; + } + echo "> Got '{$message}' [opcode: {$opcode}]\n"; + if (in_array($opcode, ['ping', 'pong'])) { + $server->send($message); + continue; + } + // Allow certain string to trigger server action + switch ($message) { + case 'exit': + echo "> Client told me to quit. Bye bye.\n"; + $server->close(); + echo "> Close status: {$server->getCloseStatus()}\n"; + exit; + case 'headers': + $server->text(implode("\r\n", $server->getRequest())); + break; + case 'ping': + $server->ping($message); + break; + case 'auth': + $auth = $server->getHeader('Authorization'); + $server->text("{$auth} - {$message}"); + break; + default: + $server->text($message); + } + } + } + } catch (ConnectionException $e) { + echo "> ERROR: {$e->getMessage()}\n"; + } +} diff --git a/vendor/textalk/websocket/examples/random_client.php b/vendor/textalk/websocket/examples/random_client.php new file mode 100644 index 0000000..b23bd6b --- /dev/null +++ b/vendor/textalk/websocket/examples/random_client.php @@ -0,0 +1,94 @@ + : The URI to connect to, default ws://localhost:8000 + * --timeout : Timeout in seconds, random default + * --fragment_size : Fragment size as bytes, random default + * --debug : Output log data (if logger is available) + */ + +namespace WebSocket; + +require __DIR__ . '/../vendor/autoload.php'; + +error_reporting(-1); + +$randStr = function (int $maxlength = 4096) { + $string = ''; + $length = rand(1, $maxlength); + for ($i = 0; $i < $length; $i++) { + $string .= chr(rand(33, 126)); + } + return $string; +}; + +echo "> Random client\n"; + +// Server options specified or random +$options = array_merge([ + 'uri' => 'ws://localhost:8000', + 'timeout' => rand(1, 60), + 'fragment_size' => rand(1, 4096) * 8, +], getopt('', ['uri:', 'timeout:', 'fragment_size:', 'debug'])); + +// If debug mode and logger is available +if (isset($options['debug']) && class_exists('WebSocket\EchoLog')) { + $logger = new EchoLog(); + $options['logger'] = $logger; + echo "> Using logger\n"; +} + +// Main loop +while (true) { + try { + $client = new Client($options['uri'], $options); + $info = json_encode([ + 'uri' => $options['uri'], + 'timeout' => $options['timeout'], + 'framgemt_size' => $client->getFragmentSize(), + ]); + echo "> Creating client {$info}\n"; + + try { + while (true) { + // Random actions + switch (rand(1, 10)) { + case 1: + echo "> Sending text\n"; + $client->text("Text message {$randStr()}"); + break; + case 2: + echo "> Sending binary\n"; + $client->binary("Binary message {$randStr()}"); + break; + case 3: + echo "> Sending close\n"; + $client->close(rand(1000, 2000), "Close message {$randStr(8)}"); + break; + case 4: + echo "> Sending ping\n"; + $client->ping("Ping message {$randStr(8)}"); + break; + case 5: + echo "> Sending pong\n"; + $client->pong("Pong message {$randStr(8)}"); + break; + default: + echo "> Receiving\n"; + $received = $client->receive(); + echo "> Received {$client->getLastOpcode()}: {$received}\n"; + } + sleep(rand(1, 5)); + } + } catch (\Throwable $e) { + echo "ERROR I/O: {$e->getMessage()} [{$e->getCode()}]\n"; + } + } catch (\Throwable $e) { + echo "ERROR: {$e->getMessage()} [{$e->getCode()}]\n"; + } + sleep(rand(1, 5)); +} diff --git a/vendor/textalk/websocket/examples/random_server.php b/vendor/textalk/websocket/examples/random_server.php new file mode 100644 index 0000000..0b0849c --- /dev/null +++ b/vendor/textalk/websocket/examples/random_server.php @@ -0,0 +1,93 @@ + : The port to listen to, default 8000 + * --timeout : Timeout in seconds, random default + * --fragment_size : Fragment size as bytes, random default + * --debug : Output log data (if logger is available) + */ + +namespace WebSocket; + +require __DIR__ . '/../vendor/autoload.php'; + +error_reporting(-1); + +$randStr = function (int $maxlength = 4096) { + $string = ''; + $length = rand(1, $maxlength); + for ($i = 0; $i < $length; $i++) { + $string .= chr(rand(33, 126)); + } + return $string; +}; + +echo "> Random server\n"; + +// Server options specified or random +$options = array_merge([ + 'port' => 8000, + 'timeout' => rand(1, 60), + 'fragment_size' => rand(1, 4096) * 8, +], getopt('', ['port:', 'timeout:', 'fragment_size:', 'debug'])); + +// If debug mode and logger is available +if (isset($options['debug']) && class_exists('WebSocket\EchoLog')) { + $logger = new EchoLog(); + $options['logger'] = $logger; + echo "> Using logger\n"; +} + +// Force quit to close server +while (true) { + try { + // Setup server + $server = new Server($options); + $info = json_encode([ + 'port' => $server->getPort(), + 'timeout' => $options['timeout'], + 'framgemt_size' => $server->getFragmentSize(), + ]); + echo "> Creating server {$info}\n"; + + while ($server->accept()) { + while (true) { + // Random actions + switch (rand(1, 10)) { + case 1: + echo "> Sending text\n"; + $server->text("Text message {$randStr()}"); + break; + case 2: + echo "> Sending binary\n"; + $server->binary("Binary message {$randStr()}"); + break; + case 3: + echo "> Sending close\n"; + $server->close(rand(1000, 2000), "Close message {$randStr(8)}"); + break; + case 4: + echo "> Sending ping\n"; + $server->ping("Ping message {$randStr(8)}"); + break; + case 5: + echo "> Sending pong\n"; + $server->pong("Pong message {$randStr(8)}"); + break; + default: + echo "> Receiving\n"; + $received = $server->receive(); + echo "> Received {$server->getLastOpcode()}: {$received}\n"; + } + sleep(rand(1, 5)); + } + } + } catch (\Throwable $e) { + echo "ERROR: {$e->getMessage()} [{$e->getCode()}]\n"; + } + sleep(rand(1, 5)); +} diff --git a/vendor/textalk/websocket/examples/send.php b/vendor/textalk/websocket/examples/send.php new file mode 100644 index 0000000..30e48e0 --- /dev/null +++ b/vendor/textalk/websocket/examples/send.php @@ -0,0 +1,51 @@ + + * + * Console options: + * --uri : The URI to connect to, default ws://localhost:8000 + * --opcode : Opcode to send, default 'text' + * --debug : Output log data (if logger is available) + */ + +namespace WebSocket; + +require __DIR__ . '/../vendor/autoload.php'; + +error_reporting(-1); + +echo "> Send client\n"; + +// Server options specified or random +$options = array_merge([ + 'uri' => 'ws://localhost:8000', + 'opcode' => 'text', +], getopt('', ['uri:', 'opcode:', 'debug'])); +$message = array_pop($argv); + +// If debug mode and logger is available +if (isset($options['debug']) && class_exists('WebSocket\EchoLog')) { + $logger = new EchoLog(); + $options['logger'] = $logger; + echo "> Using logger\n"; +} + +try { + // Create client, send and recevie + $client = new Client($options['uri'], $options); + $client->send($message, $options['opcode']); + echo "> Sent '{$message}' [opcode: {$options['opcode']}]\n"; + if (in_array($options['opcode'], ['text', 'binary'])) { + $message = $client->receive(); + $opcode = $client->getLastOpcode(); + if (!is_null($message)) { + echo "> Got '{$message}' [opcode: {$opcode}]\n"; + } + } + $client->close(); + echo "> Closing client\n"; +} catch (\Throwable $e) { + echo "ERROR: {$e->getMessage()} [{$e->getCode()}]\n"; +} diff --git a/vendor/textalk/websocket/lib/BadOpcodeException.php b/vendor/textalk/websocket/lib/BadOpcodeException.php new file mode 100644 index 0000000..260a977 --- /dev/null +++ b/vendor/textalk/websocket/lib/BadOpcodeException.php @@ -0,0 +1,14 @@ + null, + 'filter' => ['text', 'binary'], + 'fragment_size' => 4096, + 'headers' => null, + 'logger' => null, + 'origin' => null, // @deprecated + 'persistent' => false, + 'return_obj' => false, + 'timeout' => 5, + ]; + + private $socket_uri; + private $connection; + private $options = []; + private $listen = false; + private $last_opcode = null; + + + /* ---------- Magic methods ------------------------------------------------------ */ + + /** + * @param UriInterface|string $uri A ws/wss-URI + * @param array $options + * Associative array containing: + * - context: Set the stream context. Default: empty context + * - timeout: Set the socket timeout in seconds. Default: 5 + * - fragment_size: Set framgemnt size. Default: 4096 + * - headers: Associative array of headers to set/override. + */ + public function __construct($uri, array $options = []) + { + $this->socket_uri = $this->parseUri($uri); + $this->options = array_merge(self::$default_options, [ + 'logger' => new NullLogger(), + ], $options); + $this->setLogger($this->options['logger']); + } + + /** + * Get string representation of instance. + * @return string String representation. + */ + public function __toString(): string + { + return sprintf( + "%s(%s)", + get_class($this), + $this->getName() ?: 'closed' + ); + } + + + /* ---------- Client option functions -------------------------------------------- */ + + /** + * Set timeout. + * @param int $timeout Timeout in seconds. + */ + public function setTimeout(int $timeout): void + { + $this->options['timeout'] = $timeout; + if (!$this->isConnected()) { + return; + } + $this->connection->setTimeout($timeout); + $this->connection->setOptions($this->options); + } + + /** + * Set fragmentation size. + * @param int $fragment_size Fragment size in bytes. + * @return self. + */ + public function setFragmentSize(int $fragment_size): self + { + $this->options['fragment_size'] = $fragment_size; + $this->connection->setOptions($this->options); + return $this; + } + + /** + * Get fragmentation size. + * @return int $fragment_size Fragment size in bytes. + */ + public function getFragmentSize(): int + { + return $this->options['fragment_size']; + } + + + /* ---------- Connection operations ---------------------------------------------- */ + + /** + * Send text message. + * @param string $payload Content as string. + */ + public function text(string $payload): void + { + $this->send($payload); + } + + /** + * Send binary message. + * @param string $payload Content as binary string. + */ + public function binary(string $payload): void + { + $this->send($payload, 'binary'); + } + + /** + * Send ping. + * @param string $payload Optional text as string. + */ + public function ping(string $payload = ''): void + { + $this->send($payload, 'ping'); + } + + /** + * Send unsolicited pong. + * @param string $payload Optional text as string. + */ + public function pong(string $payload = ''): void + { + $this->send($payload, 'pong'); + } + + /** + * Send message. + * @param string $payload Message to send. + * @param string $opcode Opcode to use, default: 'text'. + * @param bool $masked If message should be masked default: true. + */ + public function send(string $payload, string $opcode = 'text', bool $masked = true): void + { + if (!$this->isConnected()) { + $this->connect(); + } + + if (!in_array($opcode, array_keys(self::$opcodes))) { + $warning = "Bad opcode '{$opcode}'. Try 'text' or 'binary'."; + $this->logger->warning($warning); + throw new BadOpcodeException($warning); + } + + $factory = new Factory(); + $message = $factory->create($opcode, $payload); + $this->connection->pushMessage($message, $masked); + } + + /** + * Tell the socket to close. + * @param integer $status http://tools.ietf.org/html/rfc6455#section-7.4 + * @param string $message A closing message, max 125 bytes. + */ + public function close(int $status = 1000, string $message = 'ttfn'): void + { + if (!$this->isConnected()) { + return; + } + $this->connection->close($status, $message); + } + + /** + * Disconnect from server. + */ + public function disconnect(): void + { + if ($this->isConnected()) { + $this->connection->disconnect(); + } + } + + /** + * Receive message. + * Note that this operation will block reading. + * @return mixed Message, text or null depending on settings. + */ + public function receive() + { + $filter = $this->options['filter']; + $return_obj = $this->options['return_obj']; + + if (!$this->isConnected()) { + $this->connect(); + } + + while (true) { + $message = $this->connection->pullMessage(); + $opcode = $message->getOpcode(); + if (in_array($opcode, $filter)) { + $this->last_opcode = $opcode; + $return = $return_obj ? $message : $message->getContent(); + break; + } elseif ($opcode == 'close') { + $this->last_opcode = null; + $return = $return_obj ? $message : null; + break; + } + } + return $return; + } + + + /* ---------- Connection functions ----------------------------------------------- */ + + /** + * Get last received opcode. + * @return string|null Opcode. + */ + public function getLastOpcode(): ?string + { + return $this->last_opcode; + } + + /** + * Get close status on connection. + * @return int|null Close status. + */ + public function getCloseStatus(): ?int + { + return $this->connection ? $this->connection->getCloseStatus() : null; + } + + /** + * If Client has active connection. + * @return bool True if active connection. + */ + public function isConnected(): bool + { + return $this->connection && $this->connection->isConnected(); + } + + /** + * Get name of local socket, or null if not connected. + * @return string|null + */ + public function getName(): ?string + { + return $this->isConnected() ? $this->connection->getName() : null; + } + + /** + * Get name of remote socket, or null if not connected. + * @return string|null + */ + public function getRemoteName(): ?string + { + return $this->isConnected() ? $this->connection->getRemoteName() : null; + } + + /** + * Get name of remote socket, or null if not connected. + * @return string|null + * @deprecated Will be removed in future version, use getPeer() instead. + */ + public function getPier(): ?string + { + trigger_error( + 'getPier() is deprecated and will be removed in future version. Use getRemoteName() instead.', + E_USER_DEPRECATED + ); + return $this->getRemoteName(); + } + + + /* ---------- Helper functions --------------------------------------------------- */ + + /** + * Perform WebSocket handshake + */ + protected function connect(): void + { + $this->connection = null; + + $host_uri = $this->socket_uri + ->withScheme($this->socket_uri->getScheme() == 'wss' ? 'ssl' : 'tcp') + ->withPort($this->socket_uri->getPort() ?? ($this->socket_uri->getScheme() == 'wss' ? 443 : 80)) + ->withPath('') + ->withQuery('') + ->withFragment('') + ->withUserInfo(''); + + // Path must be absolute + $http_path = $this->socket_uri->getPath(); + if ($http_path === '' || $http_path[0] !== '/') { + $http_path = "/{$http_path}"; + } + + $http_uri = (new Uri()) + ->withPath($http_path) + ->withQuery($this->socket_uri->getQuery()); + + // Set the stream context options if they're already set in the config + if (isset($this->options['context'])) { + // Suppress the error since we'll catch it below + if (@get_resource_type($this->options['context']) === 'stream-context') { + $context = $this->options['context']; + } else { + $error = "Stream context in \$options['context'] isn't a valid context."; + $this->logger->error($error); + throw new \InvalidArgumentException($error); + } + } else { + $context = stream_context_create(); + } + + $persistent = $this->options['persistent'] === true; + $flags = STREAM_CLIENT_CONNECT; + $flags = $persistent ? $flags | STREAM_CLIENT_PERSISTENT : $flags; + $socket = null; + + try { + $handler = new ErrorHandler(); + $socket = $handler->with(function () use ($host_uri, $flags, $context) { + $error = $errno = $errstr = null; + // Open the socket. + return stream_socket_client( + $host_uri, + $errno, + $errstr, + $this->options['timeout'], + $flags, + $context + ); + }); + if (!$socket) { + throw new ErrorException('No socket'); + } + } catch (ErrorException $e) { + $error = "Could not open socket to \"{$host_uri->getAuthority()}\": {$e->getMessage()} ({$e->getCode()})."; + $this->logger->error($error, ['severity' => $e->getSeverity()]); + throw new ConnectionException($error, 0, [], $e); + } + + $this->connection = new Connection($socket, $this->options); + $this->connection->setLogger($this->logger); + if (!$this->isConnected()) { + $error = "Invalid stream on \"{$host_uri->getAuthority()}\"."; + $this->logger->error($error); + throw new ConnectionException($error); + } + + if (!$persistent || $this->connection->tell() == 0) { + // Set timeout on the stream as well. + $this->connection->setTimeout($this->options['timeout']); + + // Generate the WebSocket key. + $key = self::generateKey(); + + // Default headers + $headers = [ + 'Host' => $host_uri->getAuthority(), + 'User-Agent' => 'websocket-client-php', + 'Connection' => 'Upgrade', + 'Upgrade' => 'websocket', + 'Sec-WebSocket-Key' => $key, + 'Sec-WebSocket-Version' => '13', + ]; + + // Handle basic authentication. + if ($userinfo = $this->socket_uri->getUserInfo()) { + $headers['authorization'] = 'Basic ' . base64_encode($userinfo); + } + + // Deprecated way of adding origin (use headers instead). + if (isset($this->options['origin'])) { + $headers['origin'] = $this->options['origin']; + } + + // Add and override with headers from options. + if (isset($this->options['headers'])) { + $headers = array_merge($headers, $this->options['headers']); + } + + $header = "GET {$http_uri} HTTP/1.1\r\n" . implode( + "\r\n", + array_map( + function ($key, $value) { + return "$key: $value"; + }, + array_keys($headers), + $headers + ) + ) . "\r\n\r\n"; + + // Send headers. + $this->connection->write($header); + + // Get server response header (terminated with double CR+LF). + $response = ''; + try { + do { + $buffer = $this->connection->gets(1024); + $response .= $buffer; + } while (substr_count($response, "\r\n\r\n") == 0); + } catch (Exception $e) { + throw new ConnectionException('Client handshake error', $e->getCode(), $e->getData(), $e); + } + + // Validate response. + if (!preg_match('#Sec-WebSocket-Accept:\s(.*)$#mUi', $response, $matches)) { + $error = sprintf( + "Connection to '%s' failed: Server sent invalid upgrade response: %s", + (string)$this->socket_uri, + (string)$response + ); + $this->logger->error($error); + throw new ConnectionException($error); + } + + $keyAccept = trim($matches[1]); + $expectedResonse = base64_encode( + pack('H*', sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')) + ); + + if ($keyAccept !== $expectedResonse) { + $error = 'Server sent bad upgrade response.'; + $this->logger->error($error); + throw new ConnectionException($error); + } + } + + $this->logger->info("Client connected to {$this->socket_uri}"); + } + + /** + * Generate a random string for WebSocket key. + * @return string Random string + */ + protected static function generateKey(): string + { + $key = ''; + for ($i = 0; $i < 16; $i++) { + $key .= chr(rand(33, 126)); + } + return base64_encode($key); + } + + protected function parseUri($uri): UriInterface + { + if ($uri instanceof UriInterface) { + $uri = $uri; + } elseif (is_string($uri)) { + try { + $uri = new Uri($uri); + } catch (InvalidArgumentException $e) { + throw new BadUriException("Invalid URI '{$uri}' provided.", 0, $e); + } + } else { + throw new BadUriException("Provided URI must be a UriInterface or string."); + } + if (!in_array($uri->getScheme(), ['ws', 'wss'])) { + throw new BadUriException("Invalid URI scheme, must be 'ws' or 'wss'."); + } + return $uri; + } +} diff --git a/vendor/textalk/websocket/lib/Connection.php b/vendor/textalk/websocket/lib/Connection.php new file mode 100644 index 0000000..d5aa48b --- /dev/null +++ b/vendor/textalk/websocket/lib/Connection.php @@ -0,0 +1,518 @@ +stream = $stream; + $this->setOptions($options); + $this->setLogger(new NullLogger()); + $this->msg_factory = new Factory(); + } + + public function __destruct() + { + if ($this->getType() === 'stream') { + fclose($this->stream); + } + } + + public function setOptions(array $options = []): void + { + $this->options = array_merge($this->options, $options); + } + + public function getCloseStatus(): ?int + { + return $this->close_status; + } + + /** + * Tell the socket to close. + * + * @param integer $status http://tools.ietf.org/html/rfc6455#section-7.4 + * @param string $message A closing message, max 125 bytes. + */ + public function close(int $status = 1000, string $message = 'ttfn'): void + { + if (!$this->isConnected()) { + return; + } + $status_binstr = sprintf('%016b', $status); + $status_str = ''; + foreach (str_split($status_binstr, 8) as $binstr) { + $status_str .= chr(bindec($binstr)); + } + $message = $this->msg_factory->create('close', $status_str . $message); + $this->pushMessage($message, true); + + $this->logger->debug("Closing with status: {$status}."); + + $this->is_closing = true; + while (true) { + $message = $this->pullMessage(); + if ($message->getOpcode() == 'close') { + break; + } + } + } + + + /* ---------- Message methods ---------------------------------------------------- */ + + // Push a message to stream + public function pushMessage(Message $message, bool $masked = true): void + { + $frames = $message->getFrames($masked, $this->options['fragment_size']); + foreach ($frames as $frame) { + $this->pushFrame($frame); + } + $this->logger->info("[connection] Pushed {$message}", [ + 'opcode' => $message->getOpcode(), + 'content-length' => $message->getLength(), + 'frames' => count($frames), + ]); + } + + // Pull a message from stream + public function pullMessage(): Message + { + do { + $frame = $this->pullFrame(); + $frame = $this->autoRespond($frame); + list ($final, $payload, $opcode, $masked) = $frame; + + if ($opcode == 'close') { + $this->close(); + } + + // Continuation and factual opcode + $continuation = $opcode == 'continuation'; + $payload_opcode = $continuation ? $this->read_buffer['opcode'] : $opcode; + + // First continuation frame, create buffer + if (!$final && !$continuation) { + $this->read_buffer = ['opcode' => $opcode, 'payload' => $payload, 'frames' => 1]; + continue; // Continue reading + } + + // Subsequent continuation frames, add to buffer + if ($continuation) { + $this->read_buffer['payload'] .= $payload; + $this->read_buffer['frames']++; + } + } while (!$final); + + // Final, return payload + $frames = 1; + if ($continuation) { + $payload = $this->read_buffer['payload']; + $frames = $this->read_buffer['frames']; + $this->read_buffer = null; + } + + $message = $this->msg_factory->create($payload_opcode, $payload); + + $this->logger->info("[connection] Pulled {$message}", [ + 'opcode' => $payload_opcode, + 'content-length' => strlen($payload), + 'frames' => $frames, + ]); + + return $message; + } + + + /* ---------- Frame I/O methods -------------------------------------------------- */ + + // Pull frame from stream + private function pullFrame(): array + { + // Read the fragment "header" first, two bytes. + $data = $this->read(2); + list ($byte_1, $byte_2) = array_values(unpack('C*', $data)); + $final = (bool)($byte_1 & 0b10000000); // Final fragment marker. + $rsv = $byte_1 & 0b01110000; // Unused bits, ignore + + // Parse opcode + $opcode_int = $byte_1 & 0b00001111; + $opcode_ints = array_flip(self::$opcodes); + if (!array_key_exists($opcode_int, $opcode_ints)) { + $warning = "Bad opcode in websocket frame: {$opcode_int}"; + $this->logger->warning($warning); + throw new ConnectionException($warning, ConnectionException::BAD_OPCODE); + } + $opcode = $opcode_ints[$opcode_int]; + + // Masking bit + $masked = (bool)($byte_2 & 0b10000000); + + $payload = ''; + + // Payload length + $payload_length = $byte_2 & 0b01111111; + + if ($payload_length > 125) { + if ($payload_length === 126) { + $data = $this->read(2); // 126: Payload is a 16-bit unsigned int + $payload_length = current(unpack('n', $data)); + } else { + $data = $this->read(8); // 127: Payload is a 64-bit unsigned int + $payload_length = current(unpack('J', $data)); + } + } + + // Get masking key. + if ($masked) { + $masking_key = $this->read(4); + } + + // Get the actual payload, if any (might not be for e.g. close frames. + if ($payload_length > 0) { + $data = $this->read($payload_length); + + if ($masked) { + // Unmask payload. + for ($i = 0; $i < $payload_length; $i++) { + $payload .= ($data[$i] ^ $masking_key[$i % 4]); + } + } else { + $payload = $data; + } + } + + $this->logger->debug("[connection] Pulled '{opcode}' frame", [ + 'opcode' => $opcode, + 'final' => $final, + 'content-length' => strlen($payload), + ]); + return [$final, $payload, $opcode, $masked]; + } + + // Push frame to stream + private function pushFrame(array $frame): void + { + list ($final, $payload, $opcode, $masked) = $frame; + $data = ''; + $byte_1 = $final ? 0b10000000 : 0b00000000; // Final fragment marker. + $byte_1 |= self::$opcodes[$opcode]; // Set opcode. + $data .= pack('C', $byte_1); + + $byte_2 = $masked ? 0b10000000 : 0b00000000; // Masking bit marker. + + // 7 bits of payload length... + $payload_length = strlen($payload); + if ($payload_length > 65535) { + $data .= pack('C', $byte_2 | 0b01111111); + $data .= pack('J', $payload_length); + } elseif ($payload_length > 125) { + $data .= pack('C', $byte_2 | 0b01111110); + $data .= pack('n', $payload_length); + } else { + $data .= pack('C', $byte_2 | $payload_length); + } + + // Handle masking + if ($masked) { + // generate a random mask: + $mask = ''; + for ($i = 0; $i < 4; $i++) { + $mask .= chr(rand(0, 255)); + } + $data .= $mask; + + // Append payload to frame: + for ($i = 0; $i < $payload_length; $i++) { + $data .= $payload[$i] ^ $mask[$i % 4]; + } + } else { + $data .= $payload; + } + + $this->write($data); + + $this->logger->debug("[connection] Pushed '{$opcode}' frame", [ + 'opcode' => $opcode, + 'final' => $final, + 'content-length' => strlen($payload), + ]); + } + + // Trigger auto response for frame + private function autoRespond(array $frame) + { + list ($final, $payload, $opcode, $masked) = $frame; + $payload_length = strlen($payload); + + switch ($opcode) { + case 'ping': + // If we received a ping, respond with a pong + $this->logger->debug("[connection] Received 'ping', sending 'pong'."); + $message = $this->msg_factory->create('pong', $payload); + $this->pushMessage($message, $masked); + return [$final, $payload, $opcode, $masked]; + case 'close': + // If we received close, possibly acknowledge and close connection + $status_bin = ''; + $status = ''; + if ($payload_length > 0) { + $status_bin = $payload[0] . $payload[1]; + $status = current(unpack('n', $payload)); + $this->close_status = $status; + } + // Get additional close message + if ($payload_length >= 2) { + $payload = substr($payload, 2); + } + + $this->logger->debug("[connection] Received 'close', status: {$status}."); + if (!$this->is_closing) { + $ack = "{$status_bin}Close acknowledged: {$status}"; + $message = $this->msg_factory->create('close', $ack); + $this->pushMessage($message, $masked); + } else { + $this->is_closing = false; // A close response, all done. + } + $this->disconnect(); + return [$final, $payload, $opcode, $masked]; + default: + return [$final, $payload, $opcode, $masked]; + } + } + + + /* ---------- Stream I/O methods ------------------------------------------------- */ + + /** + * Close connection stream. + * @return bool + */ + public function disconnect(): bool + { + $this->logger->debug('Closing connection'); + return fclose($this->stream); + } + + /** + * If connected to stream. + * @return bool + */ + public function isConnected(): bool + { + return in_array($this->getType(), ['stream', 'persistent stream']); + } + + /** + * Return type of connection. + * @return string|null Type of connection or null if invalid type. + */ + public function getType(): ?string + { + return get_resource_type($this->stream); + } + + /** + * Get name of local socket, or null if not connected. + * @return string|null + */ + public function getName(): ?string + { + return stream_socket_get_name($this->stream, false); + } + + /** + * Get name of remote socket, or null if not connected. + * @return string|null + */ + public function getRemoteName(): ?string + { + return stream_socket_get_name($this->stream, true); + } + + /** + * Get meta data for connection. + * @return array + */ + public function getMeta(): array + { + return stream_get_meta_data($this->stream); + } + + /** + * Returns current position of stream pointer. + * @return int + * @throws ConnectionException + */ + public function tell(): int + { + $tell = ftell($this->stream); + if ($tell === false) { + $this->throwException('Could not resolve stream pointer position'); + } + return $tell; + } + + /** + * If stream pointer is at end of file. + * @return bool + */ + public function eof(): int + { + return feof($this->stream); + } + + + /* ---------- Stream option methods ---------------------------------------------- */ + + /** + * Set time out on connection. + * @param int $seconds Timeout part in seconds + * @param int $microseconds Timeout part in microseconds + * @return bool + */ + public function setTimeout(int $seconds, int $microseconds = 0): bool + { + $this->logger->debug("Setting timeout {$seconds}:{$microseconds} seconds"); + return stream_set_timeout($this->stream, $seconds, $microseconds); + } + + + /* ---------- Stream read/write methods ------------------------------------------ */ + + /** + * Read line from stream. + * @param int $length Maximum number of bytes to read + * @param string $ending Line delimiter + * @return string Read data + */ + public function getLine(int $length, string $ending): string + { + $line = stream_get_line($this->stream, $length, $ending); + if ($line === false) { + $this->throwException('Could not read from stream'); + } + $read = strlen($line); + $this->logger->debug("Read {$read} bytes of line."); + return $line; + } + + /** + * Read line from stream. + * @param int $length Maximum number of bytes to read + * @return string Read data + */ + public function gets(int $length): string + { + $line = fgets($this->stream, $length); + if ($line === false) { + $this->throwException('Could not read from stream'); + } + $read = strlen($line); + $this->logger->debug("Read {$read} bytes of line."); + return $line; + } + + /** + * Read characters from stream. + * @param int $length Maximum number of bytes to read + * @return string Read data + */ + public function read(string $length): string + { + $data = ''; + while (strlen($data) < $length) { + $buffer = fread($this->stream, $length - strlen($data)); + if (!$buffer) { + $meta = stream_get_meta_data($this->stream); + if (!empty($meta['timed_out'])) { + $message = 'Client read timeout'; + $this->logger->error($message, $meta); + throw new TimeoutException($message, ConnectionException::TIMED_OUT, $meta); + } + } + if ($buffer === false) { + $read = strlen($data); + $this->throwException("Broken frame, read {$read} of stated {$length} bytes."); + } + if ($buffer === '') { + $this->throwException("Empty read; connection dead?"); + } + $data .= $buffer; + $read = strlen($data); + $this->logger->debug("Read {$read} of {$length} bytes."); + } + return $data; + } + + /** + * Write characters to stream. + * @param string $data Data to read + */ + public function write(string $data): void + { + $length = strlen($data); + $written = fwrite($this->stream, $data); + if ($written === false) { + $this->throwException("Failed to write {$length} bytes."); + } + if ($written < strlen($data)) { + $this->throwException("Could only write {$written} out of {$length} bytes."); + } + $this->logger->debug("Wrote {$written} of {$length} bytes."); + } + + + /* ---------- Internal helper methods -------------------------------------------- */ + + private function throwException(string $message, int $code = 0): void + { + $meta = ['closed' => true]; + if ($this->isConnected()) { + $meta = $this->getMeta(); + $this->disconnect(); + if (!empty($meta['timed_out'])) { + $this->logger->error($message, $meta); + throw new TimeoutException($message, ConnectionException::TIMED_OUT, $meta); + } + if (!empty($meta['eof'])) { + $code = ConnectionException::EOF; + } + } + $this->logger->error($message, $meta); + throw new ConnectionException($message, $code, $meta); + } +} diff --git a/vendor/textalk/websocket/lib/ConnectionException.php b/vendor/textalk/websocket/lib/ConnectionException.php new file mode 100644 index 0000000..aa1d7f4 --- /dev/null +++ b/vendor/textalk/websocket/lib/ConnectionException.php @@ -0,0 +1,33 @@ +data = $data; + } + + public function getData(): array + { + return $this->data; + } +} diff --git a/vendor/textalk/websocket/lib/Exception.php b/vendor/textalk/websocket/lib/Exception.php new file mode 100644 index 0000000..6482b7e --- /dev/null +++ b/vendor/textalk/websocket/lib/Exception.php @@ -0,0 +1,7 @@ +payload = $payload; + $this->timestamp = new DateTime(); + } + + public function getOpcode(): string + { + return $this->opcode; + } + + public function getLength(): int + { + return strlen($this->payload); + } + + public function getTimestamp(): DateTime + { + return $this->timestamp; + } + + public function getContent(): string + { + return $this->payload; + } + + public function setContent(string $payload = ''): void + { + $this->payload = $payload; + } + + public function hasContent(): bool + { + return $this->payload != ''; + } + + public function __toString(): string + { + return get_class($this); + } + + // Split messages into frames + public function getFrames(bool $masked = true, int $framesize = 4096): array + { + + $frames = []; + $split = str_split($this->getContent(), $framesize) ?: ['']; + foreach ($split as $payload) { + $frames[] = [false, $payload, 'continuation', $masked]; + } + $frames[0][2] = $this->opcode; + $frames[array_key_last($frames)][0] = true; + return $frames; + } +} diff --git a/vendor/textalk/websocket/lib/Message/Ping.php b/vendor/textalk/websocket/lib/Message/Ping.php new file mode 100644 index 0000000..f9bb652 --- /dev/null +++ b/vendor/textalk/websocket/lib/Message/Ping.php @@ -0,0 +1,15 @@ + 0, + 'text' => 1, + 'binary' => 2, + 'close' => 8, + 'ping' => 9, + 'pong' => 10, + ]; +} diff --git a/vendor/textalk/websocket/lib/Server.php b/vendor/textalk/websocket/lib/Server.php new file mode 100644 index 0000000..1521588 --- /dev/null +++ b/vendor/textalk/websocket/lib/Server.php @@ -0,0 +1,470 @@ + ['text', 'binary'], + 'fragment_size' => 4096, + 'logger' => null, + 'port' => 8000, + 'return_obj' => false, + 'timeout' => null, + ]; + + protected $port; + protected $listening; + protected $request; + protected $request_path; + private $connections = []; + private $options = []; + private $listen = false; + private $last_opcode; + + + /* ---------- Magic methods ------------------------------------------------------ */ + + /** + * @param array $options + * Associative array containing: + * - filter: Array of opcodes to handle. Default: ['text', 'binary']. + * - fragment_size: Set framgemnt size. Default: 4096 + * - logger: PSR-3 compatible logger. Default NullLogger. + * - port: Chose port for listening. Default 8000. + * - return_obj: If receive() function return Message instance. Default false. + * - timeout: Set the socket timeout in seconds. + */ + public function __construct(array $options = []) + { + $this->options = array_merge(self::$default_options, [ + 'logger' => new NullLogger(), + ], $options); + $this->port = $this->options['port']; + $this->setLogger($this->options['logger']); + + $error = $errno = $errstr = null; + set_error_handler(function (int $severity, string $message, string $file, int $line) use (&$error) { + $this->logger->warning($message, ['severity' => $severity]); + $error = $message; + }, E_ALL); + + do { + $this->listening = stream_socket_server("tcp://0.0.0.0:$this->port", $errno, $errstr); + } while ($this->listening === false && $this->port++ < 10000); + + restore_error_handler(); + + if (!$this->listening) { + $error = "Could not open listening socket: {$errstr} ({$errno}) {$error}"; + $this->logger->error($error); + throw new ConnectionException($error, (int)$errno); + } + + $this->logger->info("Server listening to port {$this->port}"); + } + + /** + * Get string representation of instance. + * @return string String representation. + */ + public function __toString(): string + { + return sprintf( + "%s(%s)", + get_class($this), + $this->getName() ?: 'closed' + ); + } + + + /* ---------- Server operations -------------------------------------------------- */ + + /** + * Accept a single incoming request. + * Note that this operation will block accepting additional requests. + * @return bool True if listening. + */ + public function accept(): bool + { + $this->disconnect(); + return (bool)$this->listening; + } + + + /* ---------- Server option functions -------------------------------------------- */ + + /** + * Get current port. + * @return int port. + */ + public function getPort(): int + { + return $this->port; + } + + /** + * Set timeout. + * @param int $timeout Timeout in seconds. + */ + public function setTimeout(int $timeout): void + { + $this->options['timeout'] = $timeout; + if (!$this->isConnected()) { + return; + } + foreach ($this->connections as $connection) { + $connection->setTimeout($timeout); + $connection->setOptions($this->options); + } + } + + /** + * Set fragmentation size. + * @param int $fragment_size Fragment size in bytes. + * @return self. + */ + public function setFragmentSize(int $fragment_size): self + { + $this->options['fragment_size'] = $fragment_size; + foreach ($this->connections as $connection) { + $connection->setOptions($this->options); + } + return $this; + } + + /** + * Get fragmentation size. + * @return int $fragment_size Fragment size in bytes. + */ + public function getFragmentSize(): int + { + return $this->options['fragment_size']; + } + + + /* ---------- Connection broadcast operations ------------------------------------ */ + + /** + * Broadcast text message to all conenctions. + * @param string $payload Content as string. + */ + public function text(string $payload): void + { + $this->send($payload); + } + + /** + * Broadcast binary message to all conenctions. + * @param string $payload Content as binary string. + */ + public function binary(string $payload): void + { + $this->send($payload, 'binary'); + } + + /** + * Broadcast ping message to all conenctions. + * @param string $payload Optional text as string. + */ + public function ping(string $payload = ''): void + { + $this->send($payload, 'ping'); + } + + /** + * Broadcast pong message to all conenctions. + * @param string $payload Optional text as string. + */ + public function pong(string $payload = ''): void + { + $this->send($payload, 'pong'); + } + + /** + * Send message on all connections. + * @param string $payload Message to send. + * @param string $opcode Opcode to use, default: 'text'. + * @param bool $masked If message should be masked default: true. + */ + public function send(string $payload, string $opcode = 'text', bool $masked = true): void + { + if (!$this->isConnected()) { + $this->connect(); + } + if (!in_array($opcode, array_keys(self::$opcodes))) { + $warning = "Bad opcode '{$opcode}'. Try 'text' or 'binary'."; + $this->logger->warning($warning); + throw new BadOpcodeException($warning); + } + + $factory = new Factory(); + $message = $factory->create($opcode, $payload); + + foreach ($this->connections as $connection) { + $connection->pushMessage($message, $masked); + } + } + + /** + * Close all connections. + * @param int $status Close status, default: 1000. + * @param string $message Close message, default: 'ttfn'. + */ + public function close(int $status = 1000, string $message = 'ttfn'): void + { + foreach ($this->connections as $connection) { + if ($connection->isConnected()) { + $connection->close($status, $message); + } + } + } + + /** + * Disconnect all connections. + */ + public function disconnect(): void + { + foreach ($this->connections as $connection) { + if ($connection->isConnected()) { + $connection->disconnect(); + } + } + $this->connections = []; + } + + /** + * Receive message from single connection. + * Note that this operation will block reading and only read from first available connection. + * @return mixed Message, text or null depending on settings. + */ + public function receive() + { + $filter = $this->options['filter']; + $return_obj = $this->options['return_obj']; + + if (!$this->isConnected()) { + $this->connect(); + } + $connection = current($this->connections); + + while (true) { + $message = $connection->pullMessage(); + $opcode = $message->getOpcode(); + if (in_array($opcode, $filter)) { + $this->last_opcode = $opcode; + $return = $return_obj ? $message : $message->getContent(); + break; + } elseif ($opcode == 'close') { + $this->last_opcode = null; + $return = $return_obj ? $message : null; + break; + } + } + return $return; + } + + + /* ---------- Connection functions ----------------------------------------------- */ + + /** + * Get requested path from last connection. + * @return string Path. + */ + public function getPath(): string + { + return $this->request_path; + } + + /** + * Get request from last connection. + * @return array Request. + */ + public function getRequest(): array + { + return $this->request; + } + + /** + * Get headers from last connection. + * @return string|null Headers. + */ + public function getHeader($header): ?string + { + foreach ($this->request as $row) { + if (stripos($row, $header) !== false) { + list($headername, $headervalue) = explode(":", $row); + return trim($headervalue); + } + } + return null; + } + + /** + * Get last received opcode. + * @return string|null Opcode. + */ + public function getLastOpcode(): ?string + { + return $this->last_opcode; + } + + /** + * Get close status from single connection. + * @return int|null Close status. + */ + public function getCloseStatus(): ?int + { + return $this->connections ? current($this->connections)->getCloseStatus() : null; + } + + /** + * If Server has active connections. + * @return bool True if active connection. + */ + public function isConnected(): bool + { + foreach ($this->connections as $connection) { + if ($connection->isConnected()) { + return true; + } + } + return false; + } + + /** + * Get name of local socket from single connection. + * @return string|null Name of local socket. + */ + public function getName(): ?string + { + return $this->isConnected() ? current($this->connections)->getName() : null; + } + + /** + * Get name of remote socket from single connection. + * @return string|null Name of remote socket. + */ + public function getRemoteName(): ?string + { + return $this->isConnected() ? current($this->connections)->getRemoteName() : null; + } + + /** + * @deprecated Will be removed in future version. + */ + public function getPier(): ?string + { + trigger_error( + 'getPier() is deprecated and will be removed in future version. Use getRemoteName() instead.', + E_USER_DEPRECATED + ); + return $this->getRemoteName(); + } + + + /* ---------- Helper functions --------------------------------------------------- */ + + // Connect when read/write operation is performed. + private function connect(): void + { + try { + $handler = new ErrorHandler(); + $socket = $handler->with(function () { + if (isset($this->options['timeout'])) { + $socket = stream_socket_accept($this->listening, $this->options['timeout']); + } else { + $socket = stream_socket_accept($this->listening); + } + if (!$socket) { + throw new ErrorException('No socket'); + } + return $socket; + }); + } catch (ErrorException $e) { + $error = "Server failed to connect. {$e->getMessage()}"; + $this->logger->error($error, ['severity' => $e->getSeverity()]); + throw new ConnectionException($error, 0, [], $e); + } + + $connection = new Connection($socket, $this->options); + $connection->setLogger($this->logger); + + if (isset($this->options['timeout'])) { + $connection->setTimeout($this->options['timeout']); + } + + $this->logger->info("Client has connected to port {port}", [ + 'port' => $this->port, + 'peer' => $connection->getRemoteName(), + ]); + $this->performHandshake($connection); + $this->connections = ['*' => $connection]; + } + + // Perform upgrade handshake on new connections. + private function performHandshake(Connection $connection): void + { + $request = ''; + do { + $buffer = $connection->getLine(1024, "\r\n"); + $request .= $buffer . "\n"; + $metadata = $connection->getMeta(); + } while (!$connection->eof() && $metadata['unread_bytes'] > 0); + + if (!preg_match('/GET (.*) HTTP\//mUi', $request, $matches)) { + $error = "No GET in request: {$request}"; + $this->logger->error($error); + throw new ConnectionException($error); + } + $get_uri = trim($matches[1]); + $uri_parts = parse_url($get_uri); + + $this->request = explode("\n", $request); + $this->request_path = $uri_parts['path']; + /// @todo Get query and fragment as well. + + if (!preg_match('#Sec-WebSocket-Key:\s(.*)$#mUi', $request, $matches)) { + $error = "Client had no Key in upgrade request: {$request}"; + $this->logger->error($error); + throw new ConnectionException($error); + } + + $key = trim($matches[1]); + + /// @todo Validate key length and base 64... + $response_key = base64_encode(pack('H*', sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'))); + + $header = "HTTP/1.1 101 Switching Protocols\r\n" + . "Upgrade: websocket\r\n" + . "Connection: Upgrade\r\n" + . "Sec-WebSocket-Accept: $response_key\r\n" + . "\r\n"; + + $connection->write($header); + $this->logger->debug("Handshake on {$get_uri}"); + } +} diff --git a/vendor/textalk/websocket/lib/TimeoutException.php b/vendor/textalk/websocket/lib/TimeoutException.php new file mode 100644 index 0000000..7276556 --- /dev/null +++ b/vendor/textalk/websocket/lib/TimeoutException.php @@ -0,0 +1,14 @@ + + + + + lib/ + + + + + tests + + + diff --git a/vendor/textalk/websocket/tests/ClientTest.php b/vendor/textalk/websocket/tests/ClientTest.php new file mode 100644 index 0000000..df23d75 --- /dev/null +++ b/vendor/textalk/websocket/tests/ClientTest.php @@ -0,0 +1,568 @@ +send('Connect'); + $this->assertTrue(MockSocket::isEmpty()); + $this->assertEquals(4096, $client->getFragmentSize()); + + MockSocket::initialize('send-receive', $this); + $client->send('Sending a message'); + $message = $client->receive(); + $this->assertTrue(MockSocket::isEmpty()); + $this->assertEquals('text', $client->getLastOpcode()); + + MockSocket::initialize('client.close', $this); + $this->assertTrue($client->isConnected()); + $this->assertNull($client->getCloseStatus()); + + $client->close(); + $this->assertFalse($client->isConnected()); + $this->assertEquals(1000, $client->getCloseStatus()); + + $this->assertTrue(MockSocket::isEmpty()); + } + + public function testDestruct(): void + { + MockSocket::initialize('client.connect', $this); + $client = new Client('ws://localhost:8000/my/mock/path'); + $client->send('Connect'); + $this->assertTrue(MockSocket::isEmpty()); + + MockSocket::initialize('client.destruct', $this); + } + + public function testClienExtendedUrl(): void + { + MockSocket::initialize('client.connect-extended', $this); + $client = new Client('ws://localhost:8000/my/mock/path?my_query=yes#my_fragment'); + $client->send('Connect'); + $this->assertTrue(MockSocket::isEmpty()); + } + + public function testClientNoPath(): void + { + MockSocket::initialize('client.connect-root', $this); + $client = new Client('ws://localhost:8000'); + $client->send('Connect'); + $this->assertTrue(MockSocket::isEmpty()); + } + + public function testClientRelativePath(): void + { + MockSocket::initialize('client.connect', $this); + $uri = new Uri('ws://localhost:8000'); + $uri = $uri->withPath('my/mock/path'); + $client = new Client($uri); + $client->send('Connect'); + $this->assertTrue(MockSocket::isEmpty()); + } + + public function testClientWsDefaultPort(): void + { + MockSocket::initialize('client.connect-default-port-ws', $this); + $uri = new Uri('ws://localhost'); + $uri = $uri->withPath('my/mock/path'); + $client = new Client($uri); + $client->send('Connect'); + $this->assertTrue(MockSocket::isEmpty()); + } + + public function testClientWssDefaultPort(): void + { + MockSocket::initialize('client.connect-default-port-wss', $this); + $uri = new Uri('wss://localhost'); + $uri = $uri->withPath('my/mock/path'); + $client = new Client($uri); + $client->send('Connect'); + $this->assertTrue(MockSocket::isEmpty()); + } + + public function testClientWithTimeout(): void + { + MockSocket::initialize('client.connect-timeout', $this); + $client = new Client('ws://localhost:8000/my/mock/path', ['timeout' => 300]); + $client->send('Connect'); + $this->assertTrue(MockSocket::isEmpty()); + } + + public function testClientWithContext(): void + { + MockSocket::initialize('client.connect-context', $this); + $client = new Client('ws://localhost:8000/my/mock/path', ['context' => '@mock-stream-context']); + $client->send('Connect'); + $this->assertTrue(MockSocket::isEmpty()); + } + + public function testClientAuthed(): void + { + MockSocket::initialize('client.connect-authed', $this); + $client = new Client('wss://usename:password@localhost:8000/my/mock/path'); + $client->send('Connect'); + $this->assertTrue(MockSocket::isEmpty()); + } + + public function testWithHeaders(): void + { + MockSocket::initialize('client.connect-headers', $this); + $client = new Client('ws://localhost:8000/my/mock/path', [ + 'origin' => 'Origin header', + 'headers' => ['Generic header' => 'Generic content'], + ]); + $client->send('Connect'); + $this->assertTrue(MockSocket::isEmpty()); + } + + public function testPayload128(): void + { + MockSocket::initialize('client.connect', $this); + $client = new Client('ws://localhost:8000/my/mock/path'); + $client->send('Connect'); + $this->assertTrue(MockSocket::isEmpty()); + + $payload = file_get_contents(__DIR__ . '/mock/payload.128.txt'); + + MockSocket::initialize('send-receive-128', $this); + $client->send($payload, 'text', false); + $message = $client->receive(); + $this->assertEquals($payload, $message); + $this->assertTrue(MockSocket::isEmpty()); + } + + public function testPayload65536(): void + { + MockSocket::initialize('client.connect', $this); + $client = new Client('ws://localhost:8000/my/mock/path'); + $client->send('Connect'); + $this->assertTrue(MockSocket::isEmpty()); + + $payload = file_get_contents(__DIR__ . '/mock/payload.65536.txt'); + $client->setFragmentSize(65540); + + MockSocket::initialize('send-receive-65536', $this); + $client->send($payload, 'text', false); + $message = $client->receive(); + $this->assertEquals($payload, $message); + $this->assertTrue(MockSocket::isEmpty()); + $this->assertEquals(65540, $client->getFragmentSize()); + } + + public function testMultiFragment(): void + { + MockSocket::initialize('client.connect', $this); + $client = new Client('ws://localhost:8000/my/mock/path'); + $client->send('Connect'); + $this->assertTrue(MockSocket::isEmpty()); + + MockSocket::initialize('send-receive-multi-fragment', $this); + $client->setFragmentSize(8); + $client->send('Multi fragment test'); + $message = $client->receive(); + $this->assertEquals('Multi fragment test', $message); + $this->assertTrue(MockSocket::isEmpty()); + $this->assertEquals(8, $client->getFragmentSize()); + } + + public function testPingPong(): void + { + MockSocket::initialize('client.connect', $this); + $client = new Client('ws://localhost:8000/my/mock/path'); + $client->send('Connect'); + $this->assertTrue(MockSocket::isEmpty()); + + MockSocket::initialize('ping-pong', $this); + $client->send('Server ping', 'ping'); + $client->send('', 'ping'); + $message = $client->receive(); + $this->assertEquals('Receiving a message', $message); + $this->assertEquals('text', $client->getLastOpcode()); + $this->assertTrue(MockSocket::isEmpty()); + } + + public function testRemoteClose(): void + { + MockSocket::initialize('client.connect', $this); + $client = new Client('ws://localhost:8000/my/mock/path'); + $client->send('Connect'); + $this->assertTrue(MockSocket::isEmpty()); + + MockSocket::initialize('close-remote', $this); + + $message = $client->receive(); + $this->assertNull($message); + + $this->assertFalse($client->isConnected()); + $this->assertEquals(17260, $client->getCloseStatus()); + $this->assertNull($client->getLastOpcode()); + $this->assertTrue(MockSocket::isEmpty()); + } + + public function testSetTimeout(): void + { + MockSocket::initialize('client.connect', $this); + $client = new Client('ws://localhost:8000/my/mock/path'); + $client->send('Connect'); + $this->assertTrue(MockSocket::isEmpty()); + + MockSocket::initialize('config-timeout', $this); + $client->setTimeout(300); + $this->assertTrue($client->isConnected()); + $this->assertTrue(MockSocket::isEmpty()); + } + + public function testReconnect(): void + { + MockSocket::initialize('client.connect', $this); + $client = new Client('ws://localhost:8000/my/mock/path'); + $client->send('Connect'); + $this->assertTrue(MockSocket::isEmpty()); + + MockSocket::initialize('client.close', $this); + $this->assertTrue($client->isConnected()); + $this->assertNull($client->getCloseStatus()); + $client->close(); + $this->assertFalse($client->isConnected()); + $this->assertEquals(1000, $client->getCloseStatus()); + $this->assertNull($client->getLastOpcode()); + $this->assertTrue(MockSocket::isEmpty()); + + MockSocket::initialize('client.reconnect', $this); + $message = $client->receive(); + $this->assertTrue($client->isConnected()); + $this->assertTrue(MockSocket::isEmpty()); + } + + public function testPersistentConnection(): void + { + MockSocket::initialize('client.connect-persistent', $this); + $client = new Client('ws://localhost:8000/my/mock/path', ['persistent' => true]); + $client->send('Connect'); + $client->disconnect(); + $this->assertFalse($client->isConnected()); + $this->assertTrue(MockSocket::isEmpty()); + } + + public function testFailedPersistentConnection(): void + { + MockSocket::initialize('client.connect-persistent-failure', $this); + $client = new Client('ws://localhost:8000/my/mock/path', ['persistent' => true]); + $this->expectException('WebSocket\ConnectionException'); + $this->expectExceptionMessage('Could not resolve stream pointer position'); + $client->send('Connect'); + } + + public function testBadScheme(): void + { + MockSocket::initialize('client.connect', $this); + $this->expectException('WebSocket\BadUriException'); + $this->expectExceptionMessage("Invalid URI scheme, must be 'ws' or 'wss'."); + $client = new Client('bad://localhost:8000/my/mock/path'); + } + + public function testBadUri(): void + { + MockSocket::initialize('client.connect', $this); + $this->expectException('WebSocket\BadUriException'); + $this->expectExceptionMessage("Invalid URI '--:this is not an uri:--' provided."); + $client = new Client('--:this is not an uri:--'); + } + + public function testInvalidUriType(): void + { + MockSocket::initialize('client.connect', $this); + $this->expectException('WebSocket\BadUriException'); + $this->expectExceptionMessage("Provided URI must be a UriInterface or string."); + $client = new Client([]); + } + + public function testUriInterface(): void + { + MockSocket::initialize('client.connect', $this); + $uri = new Uri('ws://localhost:8000/my/mock/path'); + $client = new Client($uri); + $client->send('Connect'); + $this->assertTrue(MockSocket::isEmpty()); + } + + public function testBadStreamContext(): void + { + MockSocket::initialize('client.connect-bad-context', $this); + $client = new Client('ws://localhost:8000/my/mock/path', ['context' => 'BAD']); + $this->expectException('InvalidArgumentException'); + $this->expectExceptionMessage('Stream context in $options[\'context\'] isn\'t a valid context'); + $client->send('Connect'); + } + + public function testFailedConnection(): void + { + MockSocket::initialize('client.connect-failed', $this); + $client = new Client('ws://localhost:8000/my/mock/path'); + $this->expectException('WebSocket\ConnectionException'); + $this->expectExceptionCode(0); + $this->expectExceptionMessage('Could not open socket to "localhost:8000"'); + $client->send('Connect'); + } + + public function testFailedConnectionWithError(): void + { + MockSocket::initialize('client.connect-error', $this); + $client = new Client('ws://localhost:8000/my/mock/path'); + $this->expectException('WebSocket\ConnectionException'); + $this->expectExceptionCode(0); + $this->expectExceptionMessage('Could not open socket to "localhost:8000"'); + $client->send('Connect'); + } + + public function testBadStreamConnection(): void + { + MockSocket::initialize('client.connect-bad-stream', $this); + $client = new Client('ws://localhost:8000/my/mock/path'); + $this->expectException('WebSocket\ConnectionException'); + $this->expectExceptionCode(0); + $this->expectExceptionMessage('Invalid stream on "localhost:8000"'); + $client->send('Connect'); + } + + public function testHandshakeFailure(): void + { + MockSocket::initialize('client.connect-handshake-failure', $this); + $client = new Client('ws://localhost:8000/my/mock/path'); + $this->expectException('WebSocket\ConnectionException'); + $this->expectExceptionCode(0); + $this->expectExceptionMessage('Client handshake error'); + $client->send('Connect'); + } + + public function testInvalidUpgrade(): void + { + MockSocket::initialize('client.connect-invalid-upgrade', $this); + $client = new Client('ws://localhost:8000/my/mock/path'); + $this->expectException('WebSocket\ConnectionException'); + $this->expectExceptionCode(0); + $this->expectExceptionMessage('Connection to \'ws://localhost:8000/my/mock/path\' failed'); + $client->send('Connect'); + } + + public function testInvalidKey(): void + { + MockSocket::initialize('client.connect-invalid-key', $this); + $client = new Client('ws://localhost:8000/my/mock/path'); + $this->expectException('WebSocket\ConnectionException'); + $this->expectExceptionCode(0); + $this->expectExceptionMessage('Server sent bad upgrade response'); + $client->send('Connect'); + } + + public function testSendBadOpcode(): void + { + MockSocket::initialize('client.connect', $this); + $client = new Client('ws://localhost:8000/my/mock/path'); + $client->send('Connect'); + + MockSocket::initialize('send-bad-opcode', $this); + $this->expectException('WebSocket\BadOpcodeException'); + $this->expectExceptionMessage('Bad opcode \'bad\'. Try \'text\' or \'binary\'.'); + $client->send('Bad Opcode', 'bad'); + } + + public function testRecieveBadOpcode(): void + { + MockSocket::initialize('client.connect', $this); + $client = new Client('ws://localhost:8000/my/mock/path'); + $client->send('Connect'); + MockSocket::initialize('receive-bad-opcode', $this); + $this->expectException('WebSocket\ConnectionException'); + $this->expectExceptionCode(1026); + $this->expectExceptionMessage('Bad opcode in websocket frame: 12'); + $message = $client->receive(); + } + + public function testBrokenWrite(): void + { + MockSocket::initialize('client.connect', $this); + $client = new Client('ws://localhost:8000/my/mock/path'); + $client->send('Connect'); + MockSocket::initialize('send-broken-write', $this); + $this->expectException('WebSocket\ConnectionException'); + $this->expectExceptionCode(1025); + $this->expectExceptionMessage('Could only write 18 out of 22 bytes.'); + $client->send('Failing to write'); + } + + public function testFailedWrite(): void + { + MockSocket::initialize('client.connect', $this); + $client = new Client('ws://localhost:8000/my/mock/path'); + $client->send('Connect'); + MockSocket::initialize('send-failed-write', $this); + $this->expectException('WebSocket\TimeoutException'); + $this->expectExceptionCode(1024); + $this->expectExceptionMessage('Failed to write 22 bytes.'); + $client->send('Failing to write'); + } + + public function testBrokenRead(): void + { + MockSocket::initialize('client.connect', $this); + $client = new Client('ws://localhost:8000/my/mock/path'); + $client->send('Connect'); + MockSocket::initialize('receive-broken-read', $this); + $this->expectException('WebSocket\ConnectionException'); + $this->expectExceptionCode(1025); + $this->expectExceptionMessage('Broken frame, read 0 of stated 2 bytes.'); + $client->receive(); + } + + public function testHandshakeError(): void + { + MockSocket::initialize('client.connect-handshake-error', $this); + $client = new Client('ws://localhost:8000/my/mock/path'); + $this->expectException('WebSocket\ConnectionException'); + $this->expectExceptionCode(1024); + $this->expectExceptionMessage('Client handshake error'); + $client->send('Connect'); + } + + public function testReadTimeout(): void + { + MockSocket::initialize('client.connect', $this); + $client = new Client('ws://localhost:8000/my/mock/path'); + $client->send('Connect'); + MockSocket::initialize('receive-client-timeout', $this); + $this->expectException('WebSocket\TimeoutException'); + $this->expectExceptionCode(1024); + $this->expectExceptionMessage('Client read timeout'); + $client->receive(); + } + + public function testEmptyRead(): void + { + MockSocket::initialize('client.connect', $this); + $client = new Client('ws://localhost:8000/my/mock/path'); + $client->send('Connect'); + MockSocket::initialize('receive-empty-read', $this); + $this->expectException('WebSocket\TimeoutException'); + $this->expectExceptionCode(1024); + $this->expectExceptionMessage('Empty read; connection dead?'); + $client->receive(); + } + + public function testFrameFragmentation(): void + { + MockSocket::initialize('client.connect', $this); + $client = new Client( + 'ws://localhost:8000/my/mock/path', + ['filter' => ['text', 'binary', 'pong', 'close']] + ); + $client->send('Connect'); + MockSocket::initialize('receive-fragmentation', $this); + $message = $client->receive(); + $this->assertEquals('Server ping', $message); + $this->assertEquals('pong', $client->getLastOpcode()); + $message = $client->receive(); + $this->assertEquals('Multi fragment test', $message); + $this->assertEquals('text', $client->getLastOpcode()); + $this->assertTrue(MockSocket::isEmpty()); + MockSocket::initialize('close-remote', $this); + $message = $client->receive(); + $this->assertEquals('Closing', $message); + $this->assertTrue(MockSocket::isEmpty()); + $this->assertFalse($client->isConnected()); + $this->assertEquals(17260, $client->getCloseStatus()); + $this->assertEquals('close', $client->getLastOpcode()); + } + + public function testMessageFragmentation(): void + { + MockSocket::initialize('client.connect', $this); + $client = new Client( + 'ws://localhost:8000/my/mock/path', + ['filter' => ['text', 'binary', 'pong', 'close'], 'return_obj' => true] + ); + $client->send('Connect'); + MockSocket::initialize('receive-fragmentation', $this); + $message = $client->receive(); + $this->assertInstanceOf('WebSocket\Message\Message', $message); + $this->assertInstanceOf('WebSocket\Message\Pong', $message); + $this->assertEquals('Server ping', $message->getContent()); + $this->assertEquals('pong', $message->getOpcode()); + $message = $client->receive(); + $this->assertInstanceOf('WebSocket\Message\Message', $message); + $this->assertInstanceOf('WebSocket\Message\Text', $message); + $this->assertEquals('Multi fragment test', $message->getContent()); + $this->assertEquals('text', $message->getOpcode()); + $this->assertTrue(MockSocket::isEmpty()); + MockSocket::initialize('close-remote', $this); + $message = $client->receive(); + $this->assertInstanceOf('WebSocket\Message\Message', $message); + $this->assertInstanceOf('WebSocket\Message\Close', $message); + $this->assertEquals('Closing', $message->getContent()); + $this->assertEquals('close', $message->getOpcode()); + } + + public function testConvenicanceMethods(): void + { + MockSocket::initialize('client.connect', $this); + $client = new Client('ws://localhost:8000/my/mock/path'); + $this->assertNull($client->getName()); + $this->assertNull($client->getRemoteName()); + $this->assertEquals('WebSocket\Client(closed)', "{$client}"); + $client->text('Connect'); + MockSocket::initialize('send-convenicance', $this); + $client->binary(base64_encode('Binary content')); + $client->ping(); + $client->pong(); + $this->assertEquals('127.0.0.1:12345', $client->getName()); + $this->assertEquals('127.0.0.1:8000', $client->getRemoteName()); + $this->assertEquals('WebSocket\Client(127.0.0.1:12345)', "{$client}"); + } + + public function testUnconnectedClient(): void + { + $client = new Client('ws://localhost:8000/my/mock/path'); + $this->assertFalse($client->isConnected()); + $client->setTimeout(30); + $client->close(); + $this->assertFalse($client->isConnected()); + $this->assertNull($client->getName()); + $this->assertNull($client->getRemoteName()); + $this->assertNull($client->getCloseStatus()); + } + + public function testDeprecated(): void + { + $client = new Client('ws://localhost:8000/my/mock/path'); + (new ErrorHandler())->withAll(function () use ($client) { + $this->assertNull($client->getPier()); + }, function ($exceptions, $result) { + $this->assertEquals( + 'getPier() is deprecated and will be removed in future version. Use getRemoteName() instead.', + $exceptions[0]->getMessage() + ); + }, E_USER_DEPRECATED); + } +} diff --git a/vendor/textalk/websocket/tests/ExceptionTest.php b/vendor/textalk/websocket/tests/ExceptionTest.php new file mode 100644 index 0000000..2083e70 --- /dev/null +++ b/vendor/textalk/websocket/tests/ExceptionTest.php @@ -0,0 +1,51 @@ + 'with data'], + new TimeoutException( + 'Nested exception', + ConnectionException::TIMED_OUT + ) + ); + } catch (Throwable $e) { + } + + $this->assertInstanceOf('WebSocket\ConnectionException', $e); + $this->assertInstanceOf('WebSocket\Exception', $e); + $this->assertInstanceOf('Exception', $e); + $this->assertInstanceOf('Throwable', $e); + $this->assertEquals('An error message', $e->getMessage()); + $this->assertEquals(1025, $e->getCode()); + $this->assertEquals(['test' => 'with data'], $e->getData()); + + $p = $e->getPrevious(); + $this->assertInstanceOf('WebSocket\TimeoutException', $p); + $this->assertInstanceOf('WebSocket\ConnectionException', $p); + $this->assertEquals('Nested exception', $p->getMessage()); + $this->assertEquals(1024, $p->getCode()); + $this->assertEquals([], $p->getData()); + } +} diff --git a/vendor/textalk/websocket/tests/MessageTest.php b/vendor/textalk/websocket/tests/MessageTest.php new file mode 100644 index 0000000..bade435 --- /dev/null +++ b/vendor/textalk/websocket/tests/MessageTest.php @@ -0,0 +1,60 @@ +create('text', 'Some content'); + $this->assertInstanceOf('WebSocket\Message\Text', $message); + $message = $factory->create('binary', 'Some content'); + $this->assertInstanceOf('WebSocket\Message\Binary', $message); + $message = $factory->create('ping', 'Some content'); + $this->assertInstanceOf('WebSocket\Message\Ping', $message); + $message = $factory->create('pong', 'Some content'); + $this->assertInstanceOf('WebSocket\Message\Pong', $message); + $message = $factory->create('close', 'Some content'); + $this->assertInstanceOf('WebSocket\Message\Close', $message); + } + + public function testMessage() + { + $message = new Text('Some content'); + $this->assertInstanceOf('WebSocket\Message\Message', $message); + $this->assertInstanceOf('WebSocket\Message\Text', $message); + $this->assertEquals('Some content', $message->getContent()); + $this->assertEquals('text', $message->getOpcode()); + $this->assertEquals(12, $message->getLength()); + $this->assertTrue($message->hasContent()); + $this->assertInstanceOf('DateTime', $message->getTimestamp()); + $message->setContent(''); + $this->assertEquals(0, $message->getLength()); + $this->assertFalse($message->hasContent()); + $this->assertEquals('WebSocket\Message\Text', "{$message}"); + } + + public function testBadOpcode() + { + $factory = new Factory(); + $this->expectException('WebSocket\BadOpcodeException'); + $this->expectExceptionMessage("Invalid opcode 'invalid' provided"); + $message = $factory->create('invalid', 'Some content'); + } +} diff --git a/vendor/textalk/websocket/tests/README.md b/vendor/textalk/websocket/tests/README.md new file mode 100644 index 0000000..0af2997 --- /dev/null +++ b/vendor/textalk/websocket/tests/README.md @@ -0,0 +1,28 @@ +# Testing + +Unit tests with [PHPUnit](https://phpunit.readthedocs.io/). + + +## How to run + +To run all test, run in console. + +``` +make test +``` + + +## Continuous integration + +GitHub Actions are run on PHP versions `7.4`, `8.0`, `8.1` and `8.2`. + +Code coverage by [Coveralls](https://coveralls.io/github/Textalk/websocket-php). + + +## Test strategy + +Test set up overloads various stream and socket functions, +and use "scripts" to define and mock input/output of these functions. + +This set up negates the dependency on running servers, +and allow testing various errors that might occur. diff --git a/vendor/textalk/websocket/tests/ServerTest.php b/vendor/textalk/websocket/tests/ServerTest.php new file mode 100644 index 0000000..033895f --- /dev/null +++ b/vendor/textalk/websocket/tests/ServerTest.php @@ -0,0 +1,511 @@ +assertTrue(MockSocket::isEmpty()); + MockSocket::initialize('server.accept', $this); + $server->accept(); + $server->send('Connect'); + $this->assertEquals(8000, $server->getPort()); + $this->assertEquals('/my/mock/path', $server->getPath()); + $this->assertTrue($server->isConnected()); + $this->assertEquals(4096, $server->getFragmentSize()); + $this->assertNull($server->getCloseStatus()); + $this->assertEquals([ + 'GET /my/mock/path HTTP/1.1', + 'host: localhost:8000', + 'user-agent: websocket-client-php', + 'connection: Upgrade', + 'upgrade: websocket', + 'sec-websocket-key: cktLWXhUdDQ2OXF0ZCFqOQ==', + 'sec-websocket-version: 13', + '', + '', + ], $server->getRequest()); + $this->assertEquals('websocket-client-php', $server->getHeader('USER-AGENT')); + $this->assertNull($server->getHeader('no such header')); + $this->assertTrue(MockSocket::isEmpty()); + + MockSocket::initialize('send-receive', $this); + $server->send('Sending a message'); + $message = $server->receive(); + $this->assertEquals('Receiving a message', $message); + $this->assertTrue(MockSocket::isEmpty()); + $this->assertNull($server->getCloseStatus()); + $this->assertEquals('text', $server->getLastOpcode()); + + MockSocket::initialize('server.close', $this); + $server->close(); + $this->assertFalse($server->isConnected()); + $this->assertEquals(1000, $server->getCloseStatus()); + $this->assertTrue(MockSocket::isEmpty()); + + $server->close(); // Already closed + } + + public function testDestruct(): void + { + MockSocket::initialize('server.construct', $this); + $server = new Server(); + + MockSocket::initialize('server.accept-destruct', $this); + $server->accept(); + $message = $server->receive(); + } + + public function testServerWithTimeout(): void + { + MockSocket::initialize('server.construct', $this); + $server = new Server(['timeout' => 300]); + $this->assertTrue(MockSocket::isEmpty()); + + MockSocket::initialize('server.accept-timeout', $this); + $server->accept(); + $server->send('Connect'); + $this->assertTrue(MockSocket::isEmpty()); + } + + public function testPayload128(): void + { + MockSocket::initialize('server.construct', $this); + $server = new Server(); + $this->assertTrue(MockSocket::isEmpty()); + + MockSocket::initialize('server.accept', $this); + $server->accept(); + $server->send('Connect'); + $this->assertTrue($server->isConnected()); + $this->assertTrue(MockSocket::isEmpty()); + + $payload = file_get_contents(__DIR__ . '/mock/payload.128.txt'); + + MockSocket::initialize('send-receive-128', $this); + $server->send($payload, 'text', false); + $message = $server->receive(); + $this->assertEquals($payload, $message); + $this->assertTrue(MockSocket::isEmpty()); + } + + public function testPayload65536(): void + { + MockSocket::initialize('server.construct', $this); + $server = new Server(); + $this->assertTrue(MockSocket::isEmpty()); + + MockSocket::initialize('server.accept', $this); + $server->accept(); + $server->send('Connect'); + $this->assertTrue($server->isConnected()); + $this->assertTrue(MockSocket::isEmpty()); + + $payload = file_get_contents(__DIR__ . '/mock/payload.65536.txt'); + $server->setFragmentSize(65540); + + MockSocket::initialize('send-receive-65536', $this); + $server->send($payload, 'text', false); + $message = $server->receive(); + $this->assertEquals($payload, $message); + $this->assertTrue(MockSocket::isEmpty()); + } + + public function testMultiFragment(): void + { + MockSocket::initialize('server.construct', $this); + $server = new Server(); + $this->assertTrue(MockSocket::isEmpty()); + + MockSocket::initialize('server.accept', $this); + $server->accept(); + $server->send('Connect'); + $this->assertTrue($server->isConnected()); + $this->assertTrue(MockSocket::isEmpty()); + + MockSocket::initialize('send-receive-multi-fragment', $this); + $server->setFragmentSize(8); + $server->send('Multi fragment test'); + $message = $server->receive(); + $this->assertEquals('Multi fragment test', $message); + $this->assertTrue(MockSocket::isEmpty()); + } + + public function testPingPong(): void + { + MockSocket::initialize('server.construct', $this); + $server = new Server(); + $this->assertTrue(MockSocket::isEmpty()); + + MockSocket::initialize('server.accept', $this); + $server->accept(); + $server->send('Connect'); + $this->assertTrue($server->isConnected()); + $this->assertTrue(MockSocket::isEmpty()); + + MockSocket::initialize('ping-pong', $this); + $server->send('Server ping', 'ping'); + $server->send('', 'ping'); + $message = $server->receive(); + $this->assertEquals('Receiving a message', $message); + $this->assertEquals('text', $server->getLastOpcode()); + $this->assertTrue(MockSocket::isEmpty()); + } + + public function testRemoteClose(): void + { + MockSocket::initialize('server.construct', $this); + $server = new Server(); + $this->assertTrue(MockSocket::isEmpty()); + + MockSocket::initialize('server.accept', $this); + $server->accept(); + $server->send('Connect'); + $this->assertTrue($server->isConnected()); + $this->assertTrue(MockSocket::isEmpty()); + + MockSocket::initialize('close-remote', $this); + + $message = $server->receive(); + $this->assertEquals('', $message); + + $this->assertTrue(MockSocket::isEmpty()); + $this->assertFalse($server->isConnected()); + $this->assertEquals(17260, $server->getCloseStatus()); + $this->assertNull($server->getLastOpcode()); + } + + public function testSetTimeout(): void + { + MockSocket::initialize('server.construct', $this); + $server = new Server(); + $this->assertTrue(MockSocket::isEmpty()); + + MockSocket::initialize('server.accept', $this); + $server->accept(); + $server->send('Connect'); + $this->assertTrue($server->isConnected()); + $this->assertTrue(MockSocket::isEmpty()); + + MockSocket::initialize('config-timeout', $this); + $server->setTimeout(300); + $this->assertTrue($server->isConnected()); + $this->assertTrue(MockSocket::isEmpty()); + } + + public function testFailedSocketServer(): void + { + MockSocket::initialize('server.construct-failed-socket-server', $this); + $this->expectException('WebSocket\ConnectionException'); + $this->expectExceptionCode(0); + $this->expectExceptionMessage('Could not open listening socket:'); + $server = new Server(['port' => 9999]); + } + + public function testFailedSocketServerWithError(): void + { + MockSocket::initialize('server.construct-error-socket-server', $this); + $this->expectException('WebSocket\ConnectionException'); + $this->expectExceptionCode(0); + $this->expectExceptionMessage('Could not open listening socket:'); + $server = new Server(['port' => 9999]); + } + + public function testFailedConnect(): void + { + MockSocket::initialize('server.construct', $this); + $server = new Server(); + + MockSocket::initialize('server.accept-failed-connect', $this); + $server->accept(); + $this->expectException('WebSocket\ConnectionException'); + $this->expectExceptionCode(0); + $this->expectExceptionMessage('Server failed to connect'); + $server->send('Connect'); + } + + public function testFailedConnectWithError(): void + { + MockSocket::initialize('server.construct', $this); + $server = new Server(); + + MockSocket::initialize('server.accept-error-connect', $this); + $server->accept(); + $this->expectException('WebSocket\ConnectionException'); + $this->expectExceptionCode(0); + $this->expectExceptionMessage('Server failed to connect'); + $server->send('Connect'); + } + + public function testFailedConnectTimeout(): void + { + MockSocket::initialize('server.construct', $this); + $server = new Server(['timeout' => 300]); + + MockSocket::initialize('server.accept-failed-connect', $this); + $server->accept(); + $this->expectException('WebSocket\ConnectionException'); + $this->expectExceptionCode(0); + $this->expectExceptionMessage('Server failed to connect'); + $server->send('Connect'); + } + + public function testFailedHttp(): void + { + MockSocket::initialize('server.construct', $this); + $server = new Server(); + MockSocket::initialize('server.accept-failed-http', $this); + $server->accept(); + $this->expectException('WebSocket\ConnectionException'); + $this->expectExceptionCode(0); + $this->expectExceptionMessage('No GET in request'); + $server->send('Connect'); + } + + public function testFailedWsKey(): void + { + MockSocket::initialize('server.construct', $this); + $server = new Server(); + MockSocket::initialize('server.accept-failed-ws-key', $this); + $server->accept(); + $this->expectException('WebSocket\ConnectionException'); + $this->expectExceptionCode(0); + $this->expectExceptionMessage('Client had no Key in upgrade request'); + $server->send('Connect'); + } + + public function testSendBadOpcode(): void + { + MockSocket::initialize('server.construct', $this); + $server = new Server(); + MockSocket::initialize('server.accept', $this); + $server->accept(); + $server->send('Connect'); + $this->expectException('WebSocket\BadOpcodeException'); + $this->expectExceptionCode(0); + $this->expectExceptionMessage('Bad opcode \'bad\'. Try \'text\' or \'binary\'.'); + $server->send('Bad Opcode', 'bad'); + } + + public function testRecieveBadOpcode(): void + { + MockSocket::initialize('server.construct', $this); + $server = new Server(); + MockSocket::initialize('server.accept', $this); + $server->accept(); + $server->send('Connect'); + MockSocket::initialize('receive-bad-opcode', $this); + $this->expectException('WebSocket\ConnectionException'); + $this->expectExceptionCode(1026); + $this->expectExceptionMessage('Bad opcode in websocket frame: 12'); + $message = $server->receive(); + } + + public function testBrokenWrite(): void + { + MockSocket::initialize('server.construct', $this); + $server = new Server(); + MockSocket::initialize('server.accept', $this); + $server->accept(); + $server->send('Connect'); + MockSocket::initialize('send-broken-write', $this); + $this->expectException('WebSocket\ConnectionException'); + $this->expectExceptionCode(1025); + $this->expectExceptionMessage('Could only write 18 out of 22 bytes.'); + $server->send('Failing to write'); + } + + public function testFailedWrite(): void + { + MockSocket::initialize('server.construct', $this); + $server = new Server(); + MockSocket::initialize('server.accept', $this); + $server->accept(); + $server->send('Connect'); + MockSocket::initialize('send-failed-write', $this); + $this->expectException('WebSocket\TimeoutException'); + $this->expectExceptionCode(1024); + $this->expectExceptionMessage('Failed to write 22 bytes.'); + $server->send('Failing to write'); + } + + public function testBrokenRead(): void + { + MockSocket::initialize('server.construct', $this); + $server = new Server(); + MockSocket::initialize('server.accept', $this); + $server->accept(); + $server->send('Connect'); + MockSocket::initialize('receive-broken-read', $this); + $this->expectException('WebSocket\ConnectionException'); + $this->expectExceptionCode(1025); + $this->expectExceptionMessage('Broken frame, read 0 of stated 2 bytes.'); + $server->receive(); + } + + public function testEmptyRead(): void + { + MockSocket::initialize('server.construct', $this); + $server = new Server(); + MockSocket::initialize('server.accept', $this); + $server->accept(); + $server->send('Connect'); + MockSocket::initialize('receive-empty-read', $this); + $this->expectException('WebSocket\TimeoutException'); + $this->expectExceptionCode(1024); + $this->expectExceptionMessage('Empty read; connection dead?'); + $server->receive(); + } + + public function testFrameFragmentation(): void + { + MockSocket::initialize('server.construct', $this); + $server = new Server(['filter' => ['text', 'binary', 'pong', 'close']]); + MockSocket::initialize('server.accept', $this); + $server->accept(); + $server->send('Connect'); + MockSocket::initialize('receive-fragmentation', $this); + $message = $server->receive(); + $this->assertEquals('Server ping', $message); + $this->assertEquals('pong', $server->getLastOpcode()); + $message = $server->receive(); + $this->assertEquals('Multi fragment test', $message); + $this->assertEquals('text', $server->getLastOpcode()); + $this->assertTrue(MockSocket::isEmpty()); + MockSocket::initialize('close-remote', $this); + $message = $server->receive(); + $this->assertEquals('Closing', $message); + $this->assertTrue(MockSocket::isEmpty()); + $this->assertFalse($server->isConnected()); + $this->assertEquals(17260, $server->getCloseStatus()); + $this->assertEquals('close', $server->getLastOpcode()); + } + + public function testMessageFragmentation(): void + { + MockSocket::initialize('server.construct', $this); + $server = new Server(['filter' => ['text', 'binary', 'pong', 'close'], 'return_obj' => true]); + MockSocket::initialize('server.accept', $this); + $server->accept(); + $server->send('Connect'); + MockSocket::initialize('receive-fragmentation', $this); + $message = $server->receive(); + $this->assertInstanceOf('WebSocket\Message\Message', $message); + $this->assertInstanceOf('WebSocket\Message\Pong', $message); + $this->assertEquals('Server ping', $message->getContent()); + $this->assertEquals('pong', $message->getOpcode()); + $message = $server->receive(); + $this->assertInstanceOf('WebSocket\Message\Message', $message); + $this->assertInstanceOf('WebSocket\Message\Text', $message); + $this->assertEquals('Multi fragment test', $message->getContent()); + $this->assertEquals('text', $message->getOpcode()); + $this->assertTrue(MockSocket::isEmpty()); + MockSocket::initialize('close-remote', $this); + $message = $server->receive(); + $this->assertInstanceOf('WebSocket\Message\Message', $message); + $this->assertInstanceOf('WebSocket\Message\Close', $message); + $this->assertEquals('Closing', $message->getContent()); + $this->assertEquals('close', $message->getOpcode()); + } + + public function testConvenicanceMethods(): void + { + MockSocket::initialize('server.construct', $this); + $server = new Server(); + $this->assertNull($server->getName()); + $this->assertNull($server->getRemoteName()); + $this->assertEquals('WebSocket\Server(closed)', "{$server}"); + MockSocket::initialize('server.accept', $this); + $server->accept(); + $server->text('Connect'); + MockSocket::initialize('send-convenicance', $this); + $server->binary(base64_encode('Binary content')); + $server->ping(); + $server->pong(); + $this->assertEquals('127.0.0.1:12345', $server->getName()); + $this->assertEquals('127.0.0.1:8000', $server->getRemoteName()); + $this->assertEquals('WebSocket\Server(127.0.0.1:12345)', "{$server}"); + $this->assertTrue(MockSocket::isEmpty()); + } + + public function testUnconnectedServer(): void + { + MockSocket::initialize('server.construct', $this); + $server = new Server(); + $this->assertFalse($server->isConnected()); + $server->setTimeout(30); + $server->close(); + $this->assertFalse($server->isConnected()); + $this->assertNull($server->getName()); + $this->assertNull($server->getRemoteName()); + $this->assertNull($server->getCloseStatus()); + $this->assertTrue(MockSocket::isEmpty()); + } + + public function testFailedHandshake(): void + { + MockSocket::initialize('server.construct', $this); + $server = new Server(); + $this->assertTrue(MockSocket::isEmpty()); + + MockSocket::initialize('server.accept-failed-handshake', $this); + $server->accept(); + $this->expectException('WebSocket\ConnectionException'); + $this->expectExceptionCode(0); + $this->expectExceptionMessage('Could not read from stream'); + $server->send('Connect'); + $this->assertFalse($server->isConnected()); + $this->assertTrue(MockSocket::isEmpty()); + } + + public function testServerDisconnect(): void + { + MockSocket::initialize('server.construct', $this); + $server = new Server(); + $this->assertTrue(MockSocket::isEmpty()); + MockSocket::initialize('server.accept', $this); + $server->accept(); + $server->send('Connect'); + $this->assertTrue($server->isConnected()); + $this->assertTrue(MockSocket::isEmpty()); + + MockSocket::initialize('server.disconnect', $this); + $server->disconnect(); + $this->assertFalse($server->isConnected()); + $this->assertTrue(MockSocket::isEmpty()); + } + + public function testDeprecated(): void + { + MockSocket::initialize('server.construct', $this); + $server = new Server(); + $this->assertTrue(MockSocket::isEmpty()); + (new ErrorHandler())->withAll(function () use ($server) { + $this->assertNull($server->getPier()); + }, function ($exceptions, $result) { + $this->assertEquals( + 'getPier() is deprecated and will be removed in future version. Use getRemoteName() instead.', + $exceptions[0]->getMessage() + ); + }, E_USER_DEPRECATED); + } +} diff --git a/vendor/textalk/websocket/tests/bootstrap.php b/vendor/textalk/websocket/tests/bootstrap.php new file mode 100644 index 0000000..5d6bdd0 --- /dev/null +++ b/vendor/textalk/websocket/tests/bootstrap.php @@ -0,0 +1,6 @@ +interpolate($message, $context); + $context_string = empty($context) ? '' : json_encode($context); + echo str_pad($level, 8) . " | {$message} {$context_string}\n"; + } + + public function interpolate($message, array $context = []) + { + // Build a replacement array with braces around the context keys + $replace = []; + foreach ($context as $key => $val) { + // Check that the value can be cast to string + if (!is_array($val) && (!is_object($val) || method_exists($val, '__toString'))) { + $replace['{' . $key . '}'] = $val; + } + } + + // Interpolate replacement values into the message and return + return strtr($message, $replace); + } +} diff --git a/vendor/textalk/websocket/tests/mock/MockSocket.php b/vendor/textalk/websocket/tests/mock/MockSocket.php new file mode 100644 index 0000000..e96806b --- /dev/null +++ b/vendor/textalk/websocket/tests/mock/MockSocket.php @@ -0,0 +1,81 @@ +assertEquals($current['function'], $function); + foreach ($current['params'] as $index => $param) { + if (isset($current['input-op'])) { + $param = self::op($current['input-op'], $params, $param); + } + self::$asserter->assertEquals($param, $params[$index], json_encode([$current, $params])); + } + if (isset($current['error'])) { + $map = array_merge(['msg' => 'Error', 'type' => E_USER_NOTICE], (array)$current['error']); + trigger_error($map['msg'], $map['type']); + } + if (isset($current['return-op'])) { + return self::op($current['return-op'], $params, $current['return']); + } + if (isset($current['return'])) { + return $current['return']; + } + return call_user_func_array($function, $params); + } + + // Check if all expected calls are performed + public static function isEmpty(): bool + { + return empty(self::$queue); + } + + // Initialize call queue + public static function initialize($op_file, $asserter): void + { + $file = dirname(__DIR__) . "/scripts/{$op_file}.json"; + self::$queue = json_decode(file_get_contents($file), true); + self::$asserter = $asserter; + } + + // Special output handling + private static function op($op, $params, $data) + { + switch ($op) { + case 'chr-array': + // Convert int array to string + $out = ''; + foreach ($data as $val) { + $out .= chr($val); + } + return $out; + case 'file': + $content = file_get_contents(__DIR__ . "/{$data[0]}"); + return substr($content, $data[1], $data[2]); + case 'key-save': + preg_match('#Sec-WebSocket-Key:\s(.*)$#mUi', $params[1], $matches); + self::$stored['sec-websocket-key'] = trim($matches[1]); + return str_replace('{key}', self::$stored['sec-websocket-key'], $data); + case 'key-respond': + $key = self::$stored['sec-websocket-key'] . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; + $encoded = base64_encode(pack('H*', sha1($key))); + return str_replace('{key}', $encoded, $data); + } + return $data; + } +} diff --git a/vendor/textalk/websocket/tests/mock/mock-socket.php b/vendor/textalk/websocket/tests/mock/mock-socket.php new file mode 100644 index 0000000..a038933 --- /dev/null +++ b/vendor/textalk/websocket/tests/mock/mock-socket.php @@ -0,0 +1,83 @@ +