diff options
author | marvin-borner@live.com | 2018-04-10 21:50:16 +0200 |
---|---|---|
committer | marvin-borner@live.com | 2018-04-10 21:54:48 +0200 |
commit | fc9401f04a3aca5abb22f87ebc210de8afe11d32 (patch) | |
tree | b0b310f3581764ec3955f4e496a05137a32951c3 /assets/php/vendor/react | |
parent | 286d643180672f20526f3dc3bd19d7b751e2fa97 (diff) |
Initial Commit
Diffstat (limited to 'assets/php/vendor/react')
276 files changed, 31686 insertions, 0 deletions
diff --git a/assets/php/vendor/react/cache/.gitignore b/assets/php/vendor/react/cache/.gitignore new file mode 100644 index 0000000..987e2a2 --- /dev/null +++ b/assets/php/vendor/react/cache/.gitignore @@ -0,0 +1,2 @@ +composer.lock +vendor diff --git a/assets/php/vendor/react/cache/.travis.yml b/assets/php/vendor/react/cache/.travis.yml new file mode 100644 index 0000000..290df75 --- /dev/null +++ b/assets/php/vendor/react/cache/.travis.yml @@ -0,0 +1,25 @@ +language: php + +php: +# - 5.3 # requires old distro, see below + - 5.4 + - 5.5 + - 5.6 + - 7 + - hhvm + +# lock distro so new future defaults will not break the build +dist: trusty + +matrix: + include: + - php: 5.3 + dist: precise + +sudo: false + +install: + - composer install --no-interaction + +script: + - ./vendor/bin/phpunit --coverage-text diff --git a/assets/php/vendor/react/cache/CHANGELOG.md b/assets/php/vendor/react/cache/CHANGELOG.md new file mode 100644 index 0000000..19d1801 --- /dev/null +++ b/assets/php/vendor/react/cache/CHANGELOG.md @@ -0,0 +1,35 @@ +# Changelog + +## 0.4.2 (2017-12-20) + +* Improve documentation with usage and installation instructions + (#10 by @clue) + +* Improve test suite by adding PHPUnit to `require-dev` and + add forward compatibility with PHPUnit 5 and PHPUnit 6 and + sanitize Composer autoload paths + (#14 by @shaunbramley and #12 and #18 by @clue) + +## 0.4.1 (2016-02-25) + +* Repository maintenance, split off from main repo, improve test suite and documentation +* First class support for PHP7 and HHVM (#9 by @clue) +* Adjust compatibility to 5.3 (#7 by @clue) + +## 0.4.0 (2014-02-02) + +* BC break: Bump minimum PHP version to PHP 5.4, remove 5.3 specific hacks +* BC break: Update to React/Promise 2.0 +* Dependency: Autoloading and filesystem structure now PSR-4 instead of PSR-0 + +## 0.3.2 (2013-05-10) + +* Version bump + +## 0.3.0 (2013-04-14) + +* Version bump + +## 0.2.6 (2012-12-26) + +* Feature: New cache component, used by DNS diff --git a/assets/php/vendor/react/cache/LICENSE b/assets/php/vendor/react/cache/LICENSE new file mode 100644 index 0000000..a808108 --- /dev/null +++ b/assets/php/vendor/react/cache/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2012 Igor Wiedler, Chris Boden + +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/assets/php/vendor/react/cache/README.md b/assets/php/vendor/react/cache/README.md new file mode 100644 index 0000000..70ad40a --- /dev/null +++ b/assets/php/vendor/react/cache/README.md @@ -0,0 +1,171 @@ +# Cache Component + +[](http://travis-ci.org/reactphp/cache) [](https://codeclimate.com/github/reactphp/cache) + +Async, [Promise](https://github.com/reactphp/promise)-based cache interface +for [ReactPHP](https://reactphp.org/). + +The cache component provides a +[Promise](https://github.com/reactphp/promise)-based +[`CacheInterface`](#cacheinterface) and an in-memory [`ArrayCache`](#arraycache) +implementation of that. +This allows consumers to type hint against the interface and third parties to +provide alternate implementations. + +**Table of Contents** + +* [Usage](#usage) + * [CacheInterface](#cacheinterface) + * [get()](#get) + * [set()](#set) + * [remove()](#remove) + * [ArrayCache](#arraycache) +* [Common usage](#common-usage) + * [Fallback get](#fallback-get) + * [Fallback-get-and-set](#fallback-get-and-set) +* [Install](#install) +* [Tests](#tests) +* [License](#license) + +## Usage + +### CacheInterface + +The `CacheInterface` describes the main interface of this component. +This allows consumers to type hint against the interface and third parties to +provide alternate implementations. + +#### get() + +```php +$cache + ->get('foo') + ->then('var_dump'); +``` + +This example fetches the value of the key `foo` and passes it to the +`var_dump` function. You can use any of the composition provided by +[promises](https://github.com/reactphp/promise). + +If the key `foo` does not exist, the promise will be rejected. + +#### set() + +```php +$cache->set('foo', 'bar'); +``` + +This example eventually sets the value of the key `foo` to `bar`. If it +already exists, it is overridden. No guarantees are made as to when the cache +value is set. If the cache implementation has to go over the network to store +it, it may take a while. + +#### remove() + +```php +$cache->remove('foo'); +``` + +This example eventually removes the key `foo` from the cache. As with `set`, +this may not happen instantly. + +### ArrayCache + +The `ArrayCache` provides an in-memory implementation of the +[`CacheInterface`](#cacheinterface). + +```php +$cache = new ArrayCache(); + +$cache->set('foo', 'bar'); +``` + +## Common usage + +### Fallback get + +A common use case of caches is to attempt fetching a cached value and as a +fallback retrieve it from the original data source if not found. Here is an +example of that: + +```php +$cache + ->get('foo') + ->then(null, 'getFooFromDb') + ->then('var_dump'); +``` + +First an attempt is made to retrieve the value of `foo`. A promise rejection +handler of the function `getFooFromDb` is registered. `getFooFromDb` is a +function (can be any PHP callable) that will be called if the key does not +exist in the cache. + +`getFooFromDb` can handle the missing key by returning a promise for the +actual value from the database (or any other data source). As a result, this +chain will correctly fall back, and provide the value in both cases. + +### Fallback get and set + +To expand on the fallback get example, often you want to set the value on the +cache after fetching it from the data source. + +```php +$cache + ->get('foo') + ->then(null, array($this, 'getAndCacheFooFromDb')) + ->then('var_dump'); + +public function getAndCacheFooFromDb() +{ + return $this->db + ->get('foo') + ->then(array($this, 'cacheFooFromDb')); +} + +public function cacheFooFromDb($foo) +{ + $this->cache->set('foo', $foo); + + return $foo; +} +``` + +By using chaining you can easily conditionally cache the value if it is +fetched from the database. + +## Install + +The recommended way to install this library is [through Composer](https://getcomposer.org). +[New to Composer?](https://getcomposer.org/doc/00-intro.md) + +This will install the latest supported version: + +```bash +$ composer require react/cache:^0.4.2 +``` + +See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. + +This project aims to run on any platform and thus does not require any PHP +extensions and supports running on legacy PHP 5.3 through current PHP 7+ and +HHVM. +It's *highly recommended to use PHP 7+* for this project. + +## Tests + +To run the test suite, you first need to clone this repo and then install all +dependencies [through Composer](https://getcomposer.org): + +```bash +$ composer install +``` + +To run the test suite, go to the project root and run: + +```bash +$ php vendor/bin/phpunit +``` + +## License + +MIT, see [LICENSE file](LICENSE). diff --git a/assets/php/vendor/react/cache/composer.json b/assets/php/vendor/react/cache/composer.json new file mode 100644 index 0000000..51573b6 --- /dev/null +++ b/assets/php/vendor/react/cache/composer.json @@ -0,0 +1,19 @@ +{ + "name": "react/cache", + "description": "Async, Promise-based cache interface for ReactPHP", + "keywords": ["cache", "caching", "promise", "ReactPHP"], + "license": "MIT", + "require": { + "php": ">=5.3.0", + "react/promise": "~2.0|~1.1" + }, + "autoload": { + "psr-4": { "React\\Cache\\": "src/" } + }, + "autoload-dev": { + "psr-4": { "React\\Tests\\Cache\\": "tests/" } + }, + "require-dev": { + "phpunit/phpunit": "^6.4 || ^5.7 || ^4.8.35" + } +} diff --git a/assets/php/vendor/react/cache/phpunit.xml.dist b/assets/php/vendor/react/cache/phpunit.xml.dist new file mode 100644 index 0000000..d02182f --- /dev/null +++ b/assets/php/vendor/react/cache/phpunit.xml.dist @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<phpunit colors="true" + convertErrorsToExceptions="true" + convertNoticesToExceptions="true" + convertWarningsToExceptions="true" + bootstrap="vendor/autoload.php" +> + <testsuites> + <testsuite name="React Test Suite"> + <directory>./tests/</directory> + </testsuite> + </testsuites> + + <filter> + <whitelist> + <directory>./src/</directory> + </whitelist> + </filter> +</phpunit> diff --git a/assets/php/vendor/react/cache/src/ArrayCache.php b/assets/php/vendor/react/cache/src/ArrayCache.php new file mode 100644 index 0000000..03dcc15 --- /dev/null +++ b/assets/php/vendor/react/cache/src/ArrayCache.php @@ -0,0 +1,29 @@ +<?php + +namespace React\Cache; + +use React\Promise; + +class ArrayCache implements CacheInterface +{ + private $data = array(); + + public function get($key) + { + if (!isset($this->data[$key])) { + return Promise\reject(); + } + + return Promise\resolve($this->data[$key]); + } + + public function set($key, $value) + { + $this->data[$key] = $value; + } + + public function remove($key) + { + unset($this->data[$key]); + } +} diff --git a/assets/php/vendor/react/cache/src/CacheInterface.php b/assets/php/vendor/react/cache/src/CacheInterface.php new file mode 100644 index 0000000..fd5f2d5 --- /dev/null +++ b/assets/php/vendor/react/cache/src/CacheInterface.php @@ -0,0 +1,13 @@ +<?php + +namespace React\Cache; + +interface CacheInterface +{ + // @return React\Promise\PromiseInterface + public function get($key); + + public function set($key, $value); + + public function remove($key); +} diff --git a/assets/php/vendor/react/cache/tests/ArrayCacheTest.php b/assets/php/vendor/react/cache/tests/ArrayCacheTest.php new file mode 100644 index 0000000..eec3739 --- /dev/null +++ b/assets/php/vendor/react/cache/tests/ArrayCacheTest.php @@ -0,0 +1,60 @@ +<?php + +namespace React\Tests\Cache; + +use React\Cache\ArrayCache; + +class ArrayCacheTest extends TestCase +{ + private $cache; + + public function setUp() + { + $this->cache = new ArrayCache(); + } + + /** @test */ + public function getShouldRejectPromiseForNonExistentKey() + { + $this->cache + ->get('foo') + ->then( + $this->expectCallableNever(), + $this->expectCallableOnce() + ); + } + + /** @test */ + public function setShouldSetKey() + { + $this->cache + ->set('foo', 'bar'); + + $success = $this->createCallableMock(); + $success + ->expects($this->once()) + ->method('__invoke') + ->with('bar'); + + $this->cache + ->get('foo') + ->then($success); + } + + /** @test */ + public function removeShouldRemoveKey() + { + $this->cache + ->set('foo', 'bar'); + + $this->cache + ->remove('foo'); + + $this->cache + ->get('foo') + ->then( + $this->expectCallableNever(), + $this->expectCallableOnce() + ); + } +} diff --git a/assets/php/vendor/react/cache/tests/CallableStub.php b/assets/php/vendor/react/cache/tests/CallableStub.php new file mode 100644 index 0000000..2f547cd --- /dev/null +++ b/assets/php/vendor/react/cache/tests/CallableStub.php @@ -0,0 +1,10 @@ +<?php + +namespace React\Tests\Cache; + +class CallableStub +{ + public function __invoke() + { + } +} diff --git a/assets/php/vendor/react/cache/tests/TestCase.php b/assets/php/vendor/react/cache/tests/TestCase.php new file mode 100644 index 0000000..aa449f2 --- /dev/null +++ b/assets/php/vendor/react/cache/tests/TestCase.php @@ -0,0 +1,43 @@ +<?php + +namespace React\Tests\Cache; + +use PHPUnit\Framework\TestCase as BaseTestCase; + +class TestCase extends BaseTestCase +{ + protected function expectCallableExactly($amount) + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->exactly($amount)) + ->method('__invoke'); + + return $mock; + } + + protected function expectCallableOnce() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke'); + + return $mock; + } + + protected function expectCallableNever() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->never()) + ->method('__invoke'); + + return $mock; + } + + protected function createCallableMock() + { + return $this->getMockBuilder('React\Tests\Cache\CallableStub')->getMock(); + } +} diff --git a/assets/php/vendor/react/dns/.gitignore b/assets/php/vendor/react/dns/.gitignore new file mode 100644 index 0000000..19982ea --- /dev/null +++ b/assets/php/vendor/react/dns/.gitignore @@ -0,0 +1,2 @@ +composer.lock +vendor
\ No newline at end of file diff --git a/assets/php/vendor/react/dns/.travis.yml b/assets/php/vendor/react/dns/.travis.yml new file mode 100644 index 0000000..41921e3 --- /dev/null +++ b/assets/php/vendor/react/dns/.travis.yml @@ -0,0 +1,29 @@ +language: php + +php: +# - 5.3 # requires old distro, see below + - 5.4 + - 5.5 + - 5.6 + - 7.0 + - 7.1 + - 7.2 + - hhvm # ignore errors, see below + +# lock distro so new future defaults will not break the build +dist: trusty + +matrix: + include: + - php: 5.3 + dist: precise + allow_failures: + - php: hhvm + +sudo: false + +install: + - composer install --no-interaction + +script: + - vendor/bin/phpunit --coverage-text diff --git a/assets/php/vendor/react/dns/CHANGELOG.md b/assets/php/vendor/react/dns/CHANGELOG.md new file mode 100644 index 0000000..adad0a7 --- /dev/null +++ b/assets/php/vendor/react/dns/CHANGELOG.md @@ -0,0 +1,179 @@ +# Changelog + +## 0.4.13 (2018-02-27) + +* Add `Config::loadSystemConfigBlocking()` to load default system config + and support parsing DNS config on all supported platforms + (`/etc/resolv.conf` on Unix/Linux/Mac and WMIC on Windows) + (#92, #93, #94 and #95 by @clue) + + ```php + $config = Config::loadSystemConfigBlocking(); + $server = $config->nameservers ? reset($config->nameservers) : '8.8.8.8'; + ``` + +* Remove unneeded cyclic dependency on react/socket + (#96 by @clue) + +## 0.4.12 (2018-01-14) + +* Improve test suite by adding forward compatibility with PHPUnit 6, + test against PHP 7.2, fix forward compatibility with upcoming EventLoop releases, + add test group to skip integration tests relying on internet connection + and add minor documentation improvements. + (#85 and #87 by @carusogabriel, #88 and #89 by @clue and #83 by @jsor) + +## 0.4.11 (2017-08-25) + +* Feature: Support resolving from default hosts file + (#75, #76 and #77 by @clue) + + This means that resolving hosts such as `localhost` will now work as + expected across all platforms with no changes required: + + ```php + $resolver->resolve('localhost')->then(function ($ip) { + echo 'IP: ' . $ip; + }); + ``` + + The new `HostsExecutor` exists for advanced usage and is otherwise used + internally for this feature. + +## 0.4.10 (2017-08-10) + +* Feature: Forward compatibility with EventLoop v1.0 and v0.5 and + lock minimum dependencies and work around circular dependency for tests + (#70 and #71 by @clue) + +* Fix: Work around DNS timeout issues for Windows users + (#74 by @clue) + +* Documentation and examples for advanced usage + (#66 by @WyriHaximus) + +* Remove broken TCP code, do not retry with invalid TCP query + (#73 by @clue) + +* Improve test suite by fixing HHVM build for now again and ignore future HHVM build errors and + lock Travis distro so new defaults will not break the build and + fix failing tests for PHP 7.1 + (#68 by @WyriHaximus and #69 and #72 by @clue) + +## 0.4.9 (2017-05-01) + +* Feature: Forward compatibility with upcoming Socket v1.0 and v0.8 + (#61 by @clue) + +## 0.4.8 (2017-04-16) + +* Feature: Add support for the AAAA record type to the protocol parser + (#58 by @othillo) + +* Feature: Add support for the PTR record type to the protocol parser + (#59 by @othillo) + +## 0.4.7 (2017-03-31) + +* Feature: Forward compatibility with upcoming Socket v0.6 and v0.7 component + (#57 by @clue) + +## 0.4.6 (2017-03-11) + +* Fix: Fix DNS timeout issues for Windows users and add forward compatibility + with Stream v0.5 and upcoming v0.6 + (#53 by @clue) + +* Improve test suite by adding PHPUnit to `require-dev` + (#54 by @clue) + +## 0.4.5 (2017-03-02) + +* Fix: Ensure we ignore the case of the answer + (#51 by @WyriHaximus) + +* Feature: Add `TimeoutExecutor` and simplify internal APIs to allow internal + code re-use for upcoming versions. + (#48 and #49 by @clue) + +## 0.4.4 (2017-02-13) + +* Fix: Fix handling connection and stream errors + (#45 by @clue) + +* Feature: Add examples and forward compatibility with upcoming Socket v0.5 component + (#46 and #47 by @clue) + +## 0.4.3 (2016-07-31) + +* Feature: Allow for cache adapter injection (#38 by @WyriHaximus) + + ```php + $factory = new React\Dns\Resolver\Factory(); + + $cache = new MyCustomCacheInstance(); + $resolver = $factory->createCached('8.8.8.8', $loop, $cache); + ``` + +* Feature: Support Promise cancellation (#35 by @clue) + + ```php + $promise = $resolver->resolve('reactphp.org'); + + $promise->cancel(); + ``` + +## 0.4.2 (2016-02-24) + +* Repository maintenance, split off from main repo, improve test suite and documentation +* First class support for PHP7 and HHVM (#34 by @clue) +* Adjust compatibility to 5.3 (#30 by @clue) + +## 0.4.1 (2014-04-13) + +* Bug fix: Fixed PSR-4 autoload path (@marcj/WyriHaximus) + +## 0.4.0 (2014-02-02) + +* BC break: Bump minimum PHP version to PHP 5.4, remove 5.3 specific hacks +* BC break: Update to React/Promise 2.0 +* Bug fix: Properly resolve CNAME aliases +* Dependency: Autoloading and filesystem structure now PSR-4 instead of PSR-0 +* Bump React dependencies to v0.4 + +## 0.3.2 (2013-05-10) + +* Feature: Support default port for IPv6 addresses (@clue) + +## 0.3.0 (2013-04-14) + +* Bump React dependencies to v0.3 + +## 0.2.6 (2012-12-26) + +* Feature: New cache component, used by DNS + +## 0.2.5 (2012-11-26) + +* Version bump + +## 0.2.4 (2012-11-18) + +* Feature: Change to promise-based API (@jsor) + +## 0.2.3 (2012-11-14) + +* Version bump + +## 0.2.2 (2012-10-28) + +* Feature: DNS executor timeout handling (@arnaud-lb) +* Feature: DNS retry executor (@arnaud-lb) + +## 0.2.1 (2012-10-14) + +* Minor adjustments to DNS parser + +## 0.2.0 (2012-09-10) + +* Feature: DNS resolver diff --git a/assets/php/vendor/react/dns/LICENSE b/assets/php/vendor/react/dns/LICENSE new file mode 100644 index 0000000..a808108 --- /dev/null +++ b/assets/php/vendor/react/dns/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2012 Igor Wiedler, Chris Boden + +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/assets/php/vendor/react/dns/README.md b/assets/php/vendor/react/dns/README.md new file mode 100644 index 0000000..ed86667 --- /dev/null +++ b/assets/php/vendor/react/dns/README.md @@ -0,0 +1,209 @@ +# Dns + +[](https://travis-ci.org/reactphp/dns) + +Async DNS resolver for [ReactPHP](https://reactphp.org/). + +The main point of the DNS component is to provide async DNS resolution. +However, it is really a toolkit for working with DNS messages, and could +easily be used to create a DNS server. + +**Table of contents** + +* [Basic usage](#basic-usage) +* [Caching](#caching) + * [Custom cache adapter](#custom-cache-adapter) +* [Advanced usage](#advanced-usage) + * [HostsFileExecutor](#hostsfileexecutor) +* [Install](#install) +* [Tests](#tests) +* [License](#license) +* [References](#references) + +## Basic usage + +The most basic usage is to just create a resolver through the resolver +factory. All you need to give it is a nameserver, then you can start resolving +names, baby! + +```php +$loop = React\EventLoop\Factory::create(); + +$config = React\Dns\Config\Config::loadSystemConfigBlocking(); +$server = $config->nameservers ? reset($config->nameservers) : '8.8.8.8'; + +$factory = new React\Dns\Resolver\Factory(); +$dns = $factory->create($server, $loop); + +$dns->resolve('igor.io')->then(function ($ip) { + echo "Host: $ip\n"; +}); + +$loop->run(); +``` + +See also the [first example](examples). + +The `Config` class can be used to load the system default config. This is an +operation that may access the filesystem and block. Ideally, this method should +thus be executed only once before the loop starts and not repeatedly while it is +running. +Note that this class may return an *empty* configuration if the system config +can not be loaded. As such, you'll likely want to apply a default nameserver +as above if none can be found. + +> Note that the factory loads the hosts file from the filesystem once when + creating the resolver instance. + Ideally, this method should thus be executed only once before the loop starts + and not repeatedly while it is running. + +Pending DNS queries can be cancelled by cancelling its pending promise like so: + +```php +$promise = $resolver->resolve('reactphp.org'); + +$promise->cancel(); +``` + +But there's more. + +## Caching + +You can cache results by configuring the resolver to use a `CachedExecutor`: + +```php +$loop = React\EventLoop\Factory::create(); + +$config = React\Dns\Config\Config::loadSystemConfigBlocking(); +$server = $config->nameservers ? reset($config->nameservers) : '8.8.8.8'; + +$factory = new React\Dns\Resolver\Factory(); +$dns = $factory->createCached($server, $loop); + +$dns->resolve('igor.io')->then(function ($ip) { + echo "Host: $ip\n"; +}); + +... + +$dns->resolve('igor.io')->then(function ($ip) { + echo "Host: $ip\n"; +}); + +$loop->run(); +``` + +If the first call returns before the second, only one query will be executed. +The second result will be served from an in memory cache. +This is particularly useful for long running scripts where the same hostnames +have to be looked up multiple times. + +See also the [third example](examples). + +### Custom cache adapter + +By default, the above will use an in memory cache. + +You can also specify a custom cache implementing [`CacheInterface`](https://github.com/reactphp/cache) to handle the record cache instead: + +```php +$cache = new React\Cache\ArrayCache(); +$loop = React\EventLoop\Factory::create(); +$factory = new React\Dns\Resolver\Factory(); +$dns = $factory->createCached('8.8.8.8', $loop, $cache); +``` + +See also the wiki for possible [cache implementations](https://github.com/reactphp/react/wiki/Users#cache-implementations). + +## Advanced Usage + +For more advanced usages one can utilize the `React\Dns\Query\Executor` directly. +The following example looks up the `IPv6` address for `igor.io`. + +```php +$loop = Factory::create(); + +$executor = new Executor($loop, new Parser(), new BinaryDumper(), null); + +$executor->query( + '8.8.8.8:53', + new Query($name, Message::TYPE_AAAA, Message::CLASS_IN, time()) +)->done(function (Message $message) { + foreach ($message->answers as $answer) { + echo 'IPv6: ' . $answer->data . PHP_EOL; + } +}, 'printf'); + +$loop->run(); + +``` + +See also the [fourth example](examples). + +### HostsFileExecutor + +Note that the above `Executor` class always performs an actual DNS query. +If you also want to take entries from your hosts file into account, you may +use this code: + +```php +$hosts = \React\Dns\Config\HostsFile::loadFromPathBlocking(); + +$executor = new Executor($loop, new Parser(), new BinaryDumper(), null); +$executor = new HostsFileExecutor($hosts, $executor); + +$executor->query( + '8.8.8.8:53', + new Query('localhost', Message::TYPE_A, Message::CLASS_IN, time()) +); +``` + +## Install + +The recommended way to install this library is [through Composer](https://getcomposer.org). +[New to Composer?](https://getcomposer.org/doc/00-intro.md) + +This will install the latest supported version: + +```bash +$ composer require react/dns:^0.4.13 +``` + +See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. + +This project aims to run on any platform and thus does not require any PHP +extensions and supports running on legacy PHP 5.3 through current PHP 7+ and +HHVM. +It's *highly recommended to use PHP 7+* for this project. + +## Tests + +To run the test suite, you first need to clone this repo and then install all +dependencies [through Composer](https://getcomposer.org): + +```bash +$ composer install +``` + +To run the test suite, go to the project root and run: + +```bash +$ php vendor/bin/phpunit +``` + +The test suite also contains a number of functional integration tests that rely +on a stable internet connection. +If you do not want to run these, they can simply be skipped like this: + +```bash +$ php vendor/bin/phpunit --exclude-group internet +``` + +## License + +MIT, see [LICENSE file](LICENSE). + +## References + +* [RFC 1034](https://tools.ietf.org/html/rfc1034) Domain Names - Concepts and Facilities +* [RFC 1035](https://tools.ietf.org/html/rfc1035) Domain Names - Implementation and Specification diff --git a/assets/php/vendor/react/dns/composer.json b/assets/php/vendor/react/dns/composer.json new file mode 100644 index 0000000..510a43c --- /dev/null +++ b/assets/php/vendor/react/dns/composer.json @@ -0,0 +1,24 @@ +{ + "name": "react/dns", + "description": "Async DNS resolver for ReactPHP", + "keywords": ["dns", "dns-resolver", "ReactPHP", "async"], + "license": "MIT", + "require": { + "php": ">=5.3.0", + "react/cache": "~0.4.0|~0.3.0", + "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3.5", + "react/promise": "^2.1 || ^1.2.1", + "react/promise-timer": "^1.2", + "react/stream": "^1.0 || ^0.7 || ^0.6 || ^0.5 || ^0.4.5" + }, + "require-dev": { + "clue/block-react": "^1.2", + "phpunit/phpunit": "^6.4 || ^5.7 || ^4.8.35" + }, + "autoload": { + "psr-4": { "React\\Dns\\": "src" } + }, + "autoload-dev": { + "psr-4": { "React\\Tests\\Dns\\": "tests" } + } +} diff --git a/assets/php/vendor/react/dns/examples/01-one.php b/assets/php/vendor/react/dns/examples/01-one.php new file mode 100644 index 0000000..5db164f --- /dev/null +++ b/assets/php/vendor/react/dns/examples/01-one.php @@ -0,0 +1,22 @@ +<?php + +use React\Dns\Config\Config; +use React\Dns\Resolver\Factory; + +require __DIR__ . '/../vendor/autoload.php'; + +$loop = React\EventLoop\Factory::create(); + +$config = Config::loadSystemConfigBlocking(); +$server = $config->nameservers ? reset($config->nameservers) : '8.8.8.8'; + +$factory = new Factory(); +$resolver = $factory->create($server, $loop); + +$name = isset($argv[1]) ? $argv[1] : 'www.google.com'; + +$resolver->resolve($name)->then(function ($ip) use ($name) { + echo 'IP for ' . $name . ': ' . $ip . PHP_EOL; +}, 'printf'); + +$loop->run(); diff --git a/assets/php/vendor/react/dns/examples/02-concurrent.php b/assets/php/vendor/react/dns/examples/02-concurrent.php new file mode 100644 index 0000000..87e3f5c --- /dev/null +++ b/assets/php/vendor/react/dns/examples/02-concurrent.php @@ -0,0 +1,27 @@ +<?php + +use React\Dns\Config\Config; +use React\Dns\Resolver\Factory; + +require __DIR__ . '/../vendor/autoload.php'; + +$loop = React\EventLoop\Factory::create(); + +$config = Config::loadSystemConfigBlocking(); +$server = $config->nameservers ? reset($config->nameservers) : '8.8.8.8'; + +$factory = new Factory(); +$resolver = $factory->create($server, $loop); + +$names = array_slice($argv, 1); +if (!$names) { + $names = array('google.com', 'www.google.com', 'gmail.com'); +} + +foreach ($names as $name) { + $resolver->resolve($name)->then(function ($ip) use ($name) { + echo 'IP for ' . $name . ': ' . $ip . PHP_EOL; + }, 'printf'); +} + +$loop->run(); diff --git a/assets/php/vendor/react/dns/examples/03-cached.php b/assets/php/vendor/react/dns/examples/03-cached.php new file mode 100644 index 0000000..e76a27c --- /dev/null +++ b/assets/php/vendor/react/dns/examples/03-cached.php @@ -0,0 +1,40 @@ +<?php + +use React\Dns\Config\Config; +use React\Dns\Resolver\Factory; + +require __DIR__ . '/../vendor/autoload.php'; + +$loop = React\EventLoop\Factory::create(); + +$config = Config::loadSystemConfigBlocking(); +$server = $config->nameservers ? reset($config->nameservers) : '8.8.8.8'; + +$factory = new Factory(); +$resolver = $factory->createCached($server, $loop); + +$name = isset($argv[1]) ? $argv[1] : 'www.google.com'; + +$resolver->resolve($name)->then(function ($ip) use ($name) { + echo 'IP for ' . $name . ': ' . $ip . PHP_EOL; +}, 'printf'); + +$loop->addTimer(1.0, function() use ($name, $resolver) { + $resolver->resolve($name)->then(function ($ip) use ($name) { + echo 'IP for ' . $name . ': ' . $ip . PHP_EOL; + }, 'printf'); +}); + +$loop->addTimer(2.0, function() use ($name, $resolver) { + $resolver->resolve($name)->then(function ($ip) use ($name) { + echo 'IP for ' . $name . ': ' . $ip . PHP_EOL; + }, 'printf'); +}); + +$loop->addTimer(3.0, function() use ($name, $resolver) { + $resolver->resolve($name)->then(function ($ip) use ($name) { + echo 'IP for ' . $name . ': ' . $ip . PHP_EOL; + }, 'printf'); +}); + +$loop->run(); diff --git a/assets/php/vendor/react/dns/examples/04-query-a-and-aaaa.php b/assets/php/vendor/react/dns/examples/04-query-a-and-aaaa.php new file mode 100644 index 0000000..6c46bbf --- /dev/null +++ b/assets/php/vendor/react/dns/examples/04-query-a-and-aaaa.php @@ -0,0 +1,32 @@ +<?php + +use React\Dns\Model\Message; +use React\Dns\Protocol\BinaryDumper; +use React\Dns\Protocol\Parser; +use React\Dns\Query\Executor; +use React\Dns\Query\Query; +use React\EventLoop\Factory; + +require __DIR__ . '/../vendor/autoload.php'; + +$loop = Factory::create(); + +$executor = new Executor($loop, new Parser(), new BinaryDumper(), null); + +$name = isset($argv[1]) ? $argv[1] : 'www.google.com'; + +$ipv4Query = new Query($name, Message::TYPE_A, Message::CLASS_IN, time()); +$ipv6Query = new Query($name, Message::TYPE_AAAA, Message::CLASS_IN, time()); + +$executor->query('8.8.8.8:53', $ipv4Query)->done(function (Message $message) { + foreach ($message->answers as $answer) { + echo 'IPv4: ' . $answer->data . PHP_EOL; + } +}, 'printf'); +$executor->query('8.8.8.8:53', $ipv6Query)->done(function (Message $message) { + foreach ($message->answers as $answer) { + echo 'IPv6: ' . $answer->data . PHP_EOL; + } +}, 'printf'); + +$loop->run(); diff --git a/assets/php/vendor/react/dns/phpunit.xml.dist b/assets/php/vendor/react/dns/phpunit.xml.dist new file mode 100644 index 0000000..13d3fab --- /dev/null +++ b/assets/php/vendor/react/dns/phpunit.xml.dist @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<phpunit backupGlobals="false" + backupStaticAttributes="false" + colors="true" + convertErrorsToExceptions="true" + convertNoticesToExceptions="true" + convertWarningsToExceptions="true" + processIsolation="false" + stopOnFailure="false" + syntaxCheck="false" + bootstrap="vendor/autoload.php" +> + <testsuites> + <testsuite name="React Test Suite"> + <directory>./tests/</directory> + </testsuite> + </testsuites> + + <filter> + <whitelist> + <directory>./src/</directory> + </whitelist> + </filter> +</phpunit> diff --git a/assets/php/vendor/react/dns/src/BadServerException.php b/assets/php/vendor/react/dns/src/BadServerException.php new file mode 100644 index 0000000..3bf50f1 --- /dev/null +++ b/assets/php/vendor/react/dns/src/BadServerException.php @@ -0,0 +1,7 @@ +<?php + +namespace React\Dns; + +class BadServerException extends \Exception +{ +} diff --git a/assets/php/vendor/react/dns/src/Config/Config.php b/assets/php/vendor/react/dns/src/Config/Config.php new file mode 100644 index 0000000..c82635d --- /dev/null +++ b/assets/php/vendor/react/dns/src/Config/Config.php @@ -0,0 +1,127 @@ +<?php + +namespace React\Dns\Config; + +use RuntimeException; + +class Config +{ + /** + * Loads the system DNS configuration + * + * Note that this method may block while loading its internal files and/or + * commands and should thus be used with care! While this should be + * relatively fast for most systems, it remains unknown if this may block + * under certain circumstances. In particular, this method should only be + * executed before the loop starts, not while it is running. + * + * Note that this method will try to access its files and/or commands and + * try to parse its output. Currently, this will only parse valid nameserver + * entries from its output and will ignore all other output without + * complaining. + * + * Note that the previous section implies that this may return an empty + * `Config` object if no valid nameserver entries can be found. + * + * @return self + * @codeCoverageIgnore + */ + public static function loadSystemConfigBlocking() + { + // Use WMIC output on Windows + if (DIRECTORY_SEPARATOR === '\\') { + return self::loadWmicBlocking(); + } + + // otherwise (try to) load from resolv.conf + try { + return self::loadResolvConfBlocking(); + } catch (RuntimeException $ignored) { + // return empty config if parsing fails (file not found) + return new self(); + } + } + + /** + * Loads a resolv.conf file (from the given path or default location) + * + * Note that this method blocks while loading the given path and should + * thus be used with care! While this should be relatively fast for normal + * resolv.conf files, this may be an issue if this file is located on a slow + * device or contains an excessive number of entries. In particular, this + * method should only be executed before the loop starts, not while it is + * running. + * + * Note that this method will throw if the given file can not be loaded, + * such as if it is not readable or does not exist. In particular, this file + * is not available on Windows. + * + * Currently, this will only parse valid "nameserver X" lines from the + * given file contents. Lines can be commented out with "#" and ";" and + * invalid lines will be ignored without complaining. See also + * `man resolv.conf` for more details. + * + * Note that the previous section implies that this may return an empty + * `Config` object if no valid "nameserver X" lines can be found. See also + * `man resolv.conf` which suggests that the DNS server on the localhost + * should be used in this case. This is left up to higher level consumers + * of this API. + * + * @param ?string $path (optional) path to resolv.conf file or null=load default location + * @return self + * @throws RuntimeException if the path can not be loaded (does not exist) + */ + public static function loadResolvConfBlocking($path = null) + { + if ($path === null) { + $path = '/etc/resolv.conf'; + } + + $contents = @file_get_contents($path); + if ($contents === false) { + throw new RuntimeException('Unable to load resolv.conf file "' . $path . '"'); + } + + preg_match_all('/^nameserver\s+(\S+)\s*$/m', $contents, $matches); + + $config = new self(); + $config->nameservers = $matches[1]; + + return $config; + } + + /** + * Loads the DNS configurations from Windows's WMIC (from the given command or default command) + * + * Note that this method blocks while loading the given command and should + * thus be used with care! While this should be relatively fast for normal + * WMIC commands, it remains unknown if this may block under certain + * circumstances. In particular, this method should only be executed before + * the loop starts, not while it is running. + * + * Note that this method will only try to execute the given command try to + * parse its output, irrespective of whether this command exists. In + * particular, this command is only available on Windows. Currently, this + * will only parse valid nameserver entries from the command output and will + * ignore all other output without complaining. + * + * Note that the previous section implies that this may return an empty + * `Config` object if no valid nameserver entries can be found. + * + * @param ?string $command (advanced) should not be given (NULL) unless you know what you're doing + * @return self + * @link https://ss64.com/nt/wmic.html + */ + public static function loadWmicBlocking($command = null) + { + $contents = shell_exec($command === null ? 'wmic NICCONFIG get "DNSServerSearchOrder" /format:CSV' : $command); + preg_match_all('/(?<=[{;,"])([\da-f.:]{4,})(?=[};,"])/i', $contents, $matches); + + $config = new self(); + $config->nameservers = $matches[1]; + + return $config; + } + + public $nameservers = array(); +} diff --git a/assets/php/vendor/react/dns/src/Config/FilesystemFactory.php b/assets/php/vendor/react/dns/src/Config/FilesystemFactory.php new file mode 100644 index 0000000..68cec3e --- /dev/null +++ b/assets/php/vendor/react/dns/src/Config/FilesystemFactory.php @@ -0,0 +1,73 @@ +<?php + +namespace React\Dns\Config; + +use React\EventLoop\LoopInterface; +use React\Promise; +use React\Promise\Deferred; +use React\Stream\ReadableResourceStream; +use React\Stream\Stream; + +/** + * @deprecated + * @see Config see Config class instead. + */ +class FilesystemFactory +{ + private $loop; + + public function __construct(LoopInterface $loop) + { + $this->loop = $loop; + } + + public function create($filename) + { + return $this + ->loadEtcResolvConf($filename) + ->then(array($this, 'parseEtcResolvConf')); + } + + /** + * @param string $contents + * @return Promise + * @deprecated see Config instead + */ + public function parseEtcResolvConf($contents) + { + return Promise\resolve(Config::loadResolvConfBlocking( + 'data://text/plain;base64,' . base64_encode($contents) + )); + } + + public function loadEtcResolvConf($filename) + { + if (!file_exists($filename)) { + return Promise\reject(new \InvalidArgumentException("The filename for /etc/resolv.conf given does not exist: $filename")); + } + + try { + $deferred = new Deferred(); + + $fd = fopen($filename, 'r'); + stream_set_blocking($fd, 0); + + $contents = ''; + + $stream = class_exists('React\Stream\ReadableResourceStream') ? new ReadableResourceStream($fd, $this->loop) : new Stream($fd, $this->loop); + $stream->on('data', function ($data) use (&$contents) { + $contents .= $data; + }); + $stream->on('end', function () use (&$contents, $deferred) { + $deferred->resolve($contents); + }); + $stream->on('error', function ($error) use ($deferred) { + $deferred->reject($error); + }); + + return $deferred->promise(); + } catch (\Exception $e) { + return Promise\reject($e); + } + } +} diff --git a/assets/php/vendor/react/dns/src/Config/HostsFile.php b/assets/php/vendor/react/dns/src/Config/HostsFile.php new file mode 100644 index 0000000..5b6277e --- /dev/null +++ b/assets/php/vendor/react/dns/src/Config/HostsFile.php @@ -0,0 +1,151 @@ +<?php + +namespace React\Dns\Config; + +use RuntimeException; + +/** + * Represents a static hosts file which maps hostnames to IPs + * + * Hosts files are used on most systems to avoid actually hitting the DNS for + * certain common hostnames. + * + * Most notably, this file usually contains an entry to map "localhost" to the + * local IP. Windows is a notable exception here, as Windows does not actually + * include "localhost" in this file by default. To compensate for this, this + * class may explicitly be wrapped in another HostsFile instance which + * hard-codes these entries for Windows (see also Factory). + * + * This class mostly exists to abstract the parsing/extraction process so this + * can be replaced with a faster alternative in the future. + */ +class HostsFile +{ + /** + * Returns the default path for the hosts file on this system + * + * @return string + * @codeCoverageIgnore + */ + public static function getDefaultPath() + { + // use static path for all Unix-based systems + if (DIRECTORY_SEPARATOR !== '\\') { + return '/etc/hosts'; + } + + // Windows actually stores the path in the registry under + // \HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\DataBasePath + $path = '%SystemRoot%\\system32\drivers\etc\hosts'; + + $base = getenv('SystemRoot'); + if ($base === false) { + $base = 'C:\\Windows'; + } + + return str_replace('%SystemRoot%', $base, $path); + } + + /** + * Loads a hosts file (from the given path or default location) + * + * Note that this method blocks while loading the given path and should + * thus be used with care! While this should be relatively fast for normal + * hosts file, this may be an issue if this file is located on a slow device + * or contains an excessive number of entries. In particular, this method + * should only be executed before the loop starts, not while it is running. + * + * @param ?string $path (optional) path to hosts file or null=load default location + * @return self + * @throws RuntimeException if the path can not be loaded (does not exist) + */ + public static function loadFromPathBlocking($path = null) + { + if ($path === null) { + $path = self::getDefaultPath(); + } + + $contents = @file_get_contents($path); + if ($contents === false) { + throw new RuntimeException('Unable to load hosts file "' . $path . '"'); + } + + return new self($contents); + } + + /** + * Instantiate new hosts file with the given hosts file contents + * + * @param string $contents + */ + public function __construct($contents) + { + // remove all comments from the contents + $contents = preg_replace('/[ \t]*#.*/', '', strtolower($contents)); + + $this->contents = $contents; + } + + /** + * Returns all IPs for the given hostname + * + * @param string $name + * @return string[] + */ + public function getIpsForHost($name) + { + $name = strtolower($name); + + $ips = array(); + foreach (preg_split('/\r?\n/', $this->contents) as $line) { + $parts = preg_split('/\s+/', $line); + $ip = array_shift($parts); + if ($parts && array_search($name, $parts) !== false) { + // remove IPv6 zone ID (`fe80::1%lo0` => `fe80:1`) + if (strpos($ip, ':') !== false && ($pos = strpos($ip, '%')) !== false) { + $ip = substr($ip, 0, $pos); + } + + if (@inet_pton($ip) !== false) { + $ips[] = $ip; + } + } + } + + return $ips; + } + + /** + * Returns all hostnames for the given IPv4 or IPv6 address + * + * @param string $ip + * @return string[] + */ + public function getHostsForIp($ip) + { + // check binary representation of IP to avoid string case and short notation + $ip = @inet_pton($ip); + if ($ip === false) { + return array(); + } + + $names = array(); + foreach (preg_split('/\r?\n/', $this->contents) as $line) { + $parts = preg_split('/\s+/', $line, null, PREG_SPLIT_NO_EMPTY); + $addr = array_shift($parts); + + // remove IPv6 zone ID (`fe80::1%lo0` => `fe80:1`) + if (strpos($addr, ':') !== false && ($pos = strpos($addr, '%')) !== false) { + $addr = substr($addr, 0, $pos); + } + + if (@inet_pton($addr) === $ip) { + foreach ($parts as $part) { + $names[] = $part; + } + } + } + + return $names; + } +} diff --git a/assets/php/vendor/react/dns/src/Model/HeaderBag.php b/assets/php/vendor/react/dns/src/Model/HeaderBag.php new file mode 100644 index 0000000..193e65c --- /dev/null +++ b/assets/php/vendor/react/dns/src/Model/HeaderBag.php @@ -0,0 +1,56 @@ +<?php + +namespace React\Dns\Model; + +class HeaderBag +{ + public $data = ''; + + public $attributes = array( + 'qdCount' => 0, + 'anCount' => 0, + 'nsCount' => 0, + 'arCount' => 0, + 'qr' => 0, + 'opcode' => Message::OPCODE_QUERY, + 'aa' => 0, + 'tc' => 0, + 'rd' => 0, + 'ra' => 0, + 'z' => 0, + 'rcode' => Message::RCODE_OK, + ); + + public function get($name) + { + return isset($this->attributes[$name]) ? $this->attributes[$name] : null; + } + + public function set($name, $value) + { + $this->attributes[$name] = $value; + } + + public function isQuery() + { + return 0 === $this->attributes['qr']; + } + + public function isResponse() + { + return 1 === $this->attributes['qr']; + } + + public function isTruncated() + { + return 1 === $this->attributes['tc']; + } + + public function populateCounts(Message $message) + { + $this->attributes['qdCount'] = count($message->questions); + $this->attributes['anCount'] = count($message->answers); + $this->attributes['nsCount'] = count($message->authority); + $this->attributes['arCount'] = count($message->additional); + } +} diff --git a/assets/php/vendor/react/dns/src/Model/Message.php b/assets/php/vendor/react/dns/src/Model/Message.php new file mode 100644 index 0000000..715cb1f --- /dev/null +++ b/assets/php/vendor/react/dns/src/Model/Message.php @@ -0,0 +1,100 @@ +<?php + +namespace React\Dns\Model; + +use React\Dns\Query\Query; +use React\Dns\Model\Record; + +class Message +{ + const TYPE_A = 1; + const TYPE_NS = 2; + const TYPE_CNAME = 5; + const TYPE_SOA = 6; + const TYPE_PTR = 12; + const TYPE_MX = 15; + const TYPE_TXT = 16; + const TYPE_AAAA = 28; + + const CLASS_IN = 1; + + const OPCODE_QUERY = 0; + const OPCODE_IQUERY = 1; // inverse query + const OPCODE_STATUS = 2; + + const RCODE_OK = 0; + const RCODE_FORMAT_ERROR = 1; + const RCODE_SERVER_FAILURE = 2; + const RCODE_NAME_ERROR = 3; + const RCODE_NOT_IMPLEMENTED = 4; + const RCODE_REFUSED = 5; + + /** + * Creates a new request message for the given query + * + * @param Query $query + * @return self + */ + public static function createRequestForQuery(Query $query) + { + $request = new Message(); + $request->header->set('id', self::generateId()); + $request->header->set('rd', 1); + $request->questions[] = (array) $query; + $request->prepare(); + + return $request; + } + + /** + * Creates a new response message for the given query with the given answer records + * + * @param Query $query + * @param Record[] $answers + * @return self + */ + public static function createResponseWithAnswersForQuery(Query $query, array $answers) + { + $response = new Message(); + $response->header->set('id', self::generateId()); + $response->header->set('qr', 1); + $response->header->set('opcode', Message::OPCODE_QUERY); + $response->header->set('rd', 1); + $response->header->set('rcode', Message::RCODE_OK); + + $response->questions[] = (array) $query; + + foreach ($answers as $record) { + $response->answers[] = $record; + } + + $response->prepare(); + + return $response; + } + + private static function generateId() + { + return mt_rand(0, 0xffff); + } + + public $data = ''; + + public $header; + public $questions = array(); + public $answers = array(); + public $authority = array(); + public $additional = array(); + + public $consumed = 0; + + public function __construct() + { + $this->header = new HeaderBag(); + } + + public function prepare() + { + $this->header->populateCounts($this); + } +} diff --git a/assets/php/vendor/react/dns/src/Model/Record.php b/assets/php/vendor/react/dns/src/Model/Record.php new file mode 100644 index 0000000..029d232 --- /dev/null +++ b/assets/php/vendor/react/dns/src/Model/Record.php @@ -0,0 +1,21 @@ +<?php + +namespace React\Dns\Model; + +class Record +{ + public $name; + public $type; + public $class; + public $ttl; + public $data; + + public function __construct($name, $type, $class, $ttl = 0, $data = null) + { + $this->name = $name; + $this->type = $type; + $this->class = $class; + $this->ttl = $ttl; + $this->data = $data; + } +} diff --git a/assets/php/vendor/react/dns/src/Protocol/BinaryDumper.php b/assets/php/vendor/react/dns/src/Protocol/BinaryDumper.php new file mode 100644 index 0000000..35d6ae6 --- /dev/null +++ b/assets/php/vendor/react/dns/src/Protocol/BinaryDumper.php @@ -0,0 +1,62 @@ +<?php + +namespace React\Dns\Protocol; + +use React\Dns\Model\Message; +use React\Dns\Model\HeaderBag; + +class BinaryDumper +{ + public function toBinary(Message $message) + { + $data = ''; + + $data .= $this->headerToBinary($message->header); + $data .= $this->questionToBinary($message->questions); + + return $data; + } + + private function headerToBinary(HeaderBag $header) + { + $data = ''; + + $data .= pack('n', $header->get('id')); + + $flags = 0x00; + $flags = ($flags << 1) | $header->get('qr'); + $flags = ($flags << 4) | $header->get('opcode'); + $flags = ($flags << 1) | $header->get('aa'); + $flags = ($flags << 1) | $header->get('tc'); + $flags = ($flags << 1) | $header->get('rd'); + $flags = ($flags << 1) | $header->get('ra'); + $flags = ($flags << 3) | $header->get('z'); + $flags = ($flags << 4) | $header->get('rcode'); + + $data .= pack('n', $flags); + + $data .= pack('n', $header->get('qdCount')); + $data .= pack('n', $header->get('anCount')); + $data .= pack('n', $header->get('nsCount')); + $data .= pack('n', $header->get('arCount')); + + return $data; + } + + private function questionToBinary(array $questions) + { + $data = ''; + + foreach ($questions as $question) { + $labels = explode('.', $question['name']); + foreach ($labels as $label) { + $data .= chr(strlen($label)).$label; + } + $data .= "\x00"; + + $data .= pack('n*', $question['type'], $question['class']); + } + + return $data; + } +} diff --git a/assets/php/vendor/react/dns/src/Protocol/Parser.php b/assets/php/vendor/react/dns/src/Protocol/Parser.php new file mode 100644 index 0000000..1191cd3 --- /dev/null +++ b/assets/php/vendor/react/dns/src/Protocol/Parser.php @@ -0,0 +1,254 @@ +<?php + +namespace React\Dns\Protocol; + +use React\Dns\Model\Message; +use React\Dns\Model\Record; +use InvalidArgumentException; + +/** + * DNS protocol parser + * + * Obsolete and uncommon types and classes are not implemented. + */ +class Parser +{ + /** + * Parses the given raw binary message into a Message object + * + * @param string $data + * @throws InvalidArgumentException + * @return Message + */ + public function parseMessage($data) + { + $message = new Message(); + if ($this->parse($data, $message) !== $message) { + throw new InvalidArgumentException('Unable to parse binary message'); + } + + return $message; + } + + /** + * @deprecated unused, exists for BC only + */ + public function parseChunk($data, Message $message) + { + return $this->parse($data, $message); + } + + private function parse($data, Message $message) + { + $message->data .= $data; + + if (!$message->header->get('id')) { + if (!$this->parseHeader($message)) { + return; + } + } + + if ($message->header->get('qdCount') != count($message->questions)) { + if (!$this->parseQuestion($message)) { + return; + } + } + + if ($message->header->get('anCount') != count($message->answers)) { + if (!$this->parseAnswer($message)) { + return; + } + } + + return $message; + } + + public function parseHeader(Message $message) + { + if (strlen($message->data) < 12) { + return; + } + + $header = substr($message->data, 0, 12); + $message->consumed += 12; + + list($id, $fields, $qdCount, $anCount, $nsCount, $arCount) = array_values(unpack('n*', $header)); + + $rcode = $fields & bindec('1111'); + $z = ($fields >> 4) & bindec('111'); + $ra = ($fields >> 7) & 1; + $rd = ($fields >> 8) & 1; + $tc = ($fields >> 9) & 1; + $aa = ($fields >> 10) & 1; + $opcode = ($fields >> 11) & bindec('1111'); + $qr = ($fields >> 15) & 1; + + $vars = compact('id', 'qdCount', 'anCount', 'nsCount', 'arCount', + 'qr', 'opcode', 'aa', 'tc', 'rd', 'ra', 'z', 'rcode'); + + + foreach ($vars as $name => $value) { + $message->header->set($name, $value); + } + + return $message; + } + + public function parseQuestion(Message $message) + { + if (strlen($message->data) < 2) { + return; + } + + $consumed = $message->consumed; + + list($labels, $consumed) = $this->readLabels($message->data, $consumed); + + if (null === $labels) { + return; + } + + if (strlen($message->data) - $consumed < 4) { + return; + } + + list($type, $class) = array_values(unpack('n*', substr($message->data, $consumed, 4))); + $consumed += 4; + + $message->consumed = $consumed; + + $message->questions[] = array( + 'name' => implode('.', $labels), + 'type' => $type, + 'class' => $class, + ); + + if ($message->header->get('qdCount') != count($message->questions)) { + return $this->parseQuestion($message); + } + + return $message; + } + + public function parseAnswer(Message $message) + { + if (strlen($message->data) < 2) { + return; + } + + $consumed = $message->consumed; + + list($labels, $consumed) = $this->readLabels($message->data, $consumed); + + if (null === $labels) { + return; + } + + if (strlen($message->data) - $consumed < 10) { + return; + } + + list($type, $class) = array_values(unpack('n*', substr($message->data, $consumed, 4))); + $consumed += 4; + + list($ttl) = array_values(unpack('N', substr($message->data, $consumed, 4))); + $consumed += 4; + + list($rdLength) = array_values(unpack('n', substr($message->data, $consumed, 2))); + $consumed += 2; + + $rdata = null; + + if (Message::TYPE_A === $type || Message::TYPE_AAAA === $type) { + $ip = substr($message->data, $consumed, $rdLength); + $consumed += $rdLength; + + $rdata = inet_ntop($ip); + } + + if (Message::TYPE_CNAME === $type || Message::TYPE_PTR === $type) { + list($bodyLabels, $consumed) = $this->readLabels($message->data, $consumed); + + $rdata = implode('.', $bodyLabels); + } + + $message->consumed = $consumed; + + $name = implode('.', $labels); + $ttl = $this->signedLongToUnsignedLong($ttl); + $record = new Record($name, $type, $class, $ttl, $rdata); + + $message->answers[] = $record; + + if ($message->header->get('anCount') != count($message->answers)) { + return $this->parseAnswer($message); + } + + return $message; + } + + private function readLabels($data, $consumed) + { + $labels = array(); + + while (true) { + if ($this->isEndOfLabels($data, $consumed)) { + $consumed += 1; + break; + } + + if ($this->isCompressedLabel($data, $consumed)) { + list($newLabels, $consumed) = $this->getCompressedLabel($data, $consumed); + $labels = array_merge($labels, $newLabels); + break; + } + + $length = ord(substr($data, $consumed, 1)); + $consumed += 1; + + if (strlen($data) - $consumed < $length) { + return array(null, null); + } + + $labels[] = substr($data, $consumed, $length); + $consumed += $length; + } + + return array($labels, $consumed); + } + + public function isEndOfLabels($data, $consumed) + { + $length = ord(substr($data, $consumed, 1)); + return 0 === $length; + } + + public function getCompressedLabel($data, $consumed) + { + list($nameOffset, $consumed) = $this->getCompressedLabelOffset($data, $consumed); + list($labels) = $this->readLabels($data, $nameOffset); + + return array($labels, $consumed); + } + + public function isCompressedLabel($data, $consumed) + { + $mask = 0xc000; // 1100000000000000 + list($peek) = array_values(unpack('n', substr($data, $consumed, 2))); + + return (bool) ($peek & $mask); + } + + public function getCompressedLabelOffset($data, $consumed) + { + $mask = 0x3fff; // 0011111111111111 + list($peek) = array_values(unpack('n', substr($data, $consumed, 2))); + + return array($peek & $mask, $consumed + 2); + } + + public function signedLongToUnsignedLong($i) + { + return $i & 0x80000000 ? $i - 0xffffffff : $i; + } +} diff --git a/assets/php/vendor/react/dns/src/Query/CachedExecutor.php b/assets/php/vendor/react/dns/src/Query/CachedExecutor.php new file mode 100644 index 0000000..285936d --- /dev/null +++ b/assets/php/vendor/react/dns/src/Query/CachedExecutor.php @@ -0,0 +1,55 @@ +<?php + +namespace React\Dns\Query; + +use React\Dns\Model\Message; + +class CachedExecutor implements ExecutorInterface +{ + private $executor; + private $cache; + + public function __construct(ExecutorInterface $executor, RecordCache $cache) + { + $this->executor = $executor; + $this->cache = $cache; + } + + public function query($nameserver, Query $query) + { + $executor = $this->executor; + $cache = $this->cache; + + return $this->cache + ->lookup($query) + ->then( + function ($cachedRecords) use ($query) { + return Message::createResponseWithAnswersForQuery($query, $cachedRecords); + }, + function () use ($executor, $cache, $nameserver, $query) { + return $executor + ->query($nameserver, $query) + ->then(function ($response) use ($cache, $query) { + $cache->storeResponseMessage($query->currentTime, $response); + return $response; + }); + } + ); + } + + /** + * @deprecated unused, exists for BC only + */ + public function buildResponse(Query $query, array $cachedRecords) + { + return Message::createResponseWithAnswersForQuery($query, $cachedRecords); + } + + /** + * @deprecated unused, exists for BC only + */ + protected function generateId() + { + return mt_rand(0, 0xffff); + } +} diff --git a/assets/php/vendor/react/dns/src/Query/CancellationException.php b/assets/php/vendor/react/dns/src/Query/CancellationException.php new file mode 100644 index 0000000..ac30f4c --- /dev/null +++ b/assets/php/vendor/react/dns/src/Query/CancellationException.php @@ -0,0 +1,7 @@ +<?php + +namespace React\Dns\Query; + +class CancellationException extends \RuntimeException +{ +} diff --git a/assets/php/vendor/react/dns/src/Query/Executor.php b/assets/php/vendor/react/dns/src/Query/Executor.php new file mode 100644 index 0000000..4c51f2b --- /dev/null +++ b/assets/php/vendor/react/dns/src/Query/Executor.php @@ -0,0 +1,156 @@ +<?php + +namespace React\Dns\Query; + +use React\Dns\Model\Message; +use React\Dns\Protocol\Parser; +use React\Dns\Protocol\BinaryDumper; +use React\EventLoop\LoopInterface; +use React\Promise\Deferred; +use React\Promise; +use React\Stream\DuplexResourceStream; +use React\Stream\Stream; + +class Executor implements ExecutorInterface +{ + private $loop; + private $parser; + private $dumper; + private $timeout; + + /** + * + * Note that albeit supported, the $timeout parameter is deprecated! + * You should pass a `null` value here instead. If you need timeout handling, + * use the `TimeoutConnector` instead. + * + * @param LoopInterface $loop + * @param Parser $parser + * @param BinaryDumper $dumper + * @param null|float $timeout DEPRECATED: timeout for DNS query or NULL=no timeout + */ + public function __construct(LoopInterface $loop, Parser $parser, BinaryDumper $dumper, $timeout = 5) + { + $this->loop = $loop; + $this->parser = $parser; + $this->dumper = $dumper; + $this->timeout = $timeout; + } + + public function query($nameserver, Query $query) + { + $request = Message::createRequestForQuery($query); + + $queryData = $this->dumper->toBinary($request); + $transport = strlen($queryData) > 512 ? 'tcp' : 'udp'; + + return $this->doQuery($nameserver, $transport, $queryData, $query->name); + } + + /** + * @deprecated unused, exists for BC only + */ + public function prepareRequest(Query $query) + { + return Message::createRequestForQuery($query); + } + + public function doQuery($nameserver, $transport, $queryData, $name) + { + // we only support UDP right now + if ($transport !== 'udp') { + return Promise\reject(new \RuntimeException( + 'DNS query for ' . $name . ' failed: Requested transport "' . $transport . '" not available, only UDP is supported in this version' + )); + } + + $that = $this; + $parser = $this->parser; + $loop = $this->loop; + + // UDP connections are instant, so try this without a timer + try { + $conn = $this->createConnection($nameserver, $transport); + } catch (\Exception $e) { + return Promise\reject(new \RuntimeException('DNS query for ' . $name . ' failed: ' . $e->getMessage(), 0, $e)); + } + + $deferred = new Deferred(function ($resolve, $reject) use (&$timer, $loop, &$conn, $name) { + $reject(new CancellationException(sprintf('DNS query for %s has been cancelled', $name))); + + if ($timer !== null) { + $loop->cancelTimer($timer); + } + $conn->close(); + }); + + $timer = null; + if ($this->timeout !== null) { + $timer = $this->loop->addTimer($this->timeout, function () use (&$conn, $name, $deferred) { + $conn->close(); + $deferred->reject(new TimeoutException(sprintf("DNS query for %s timed out", $name))); + }); + } + + $conn->on('data', function ($data) use ($conn, $parser, $deferred, $timer, $loop, $name) { + $conn->end(); + if ($timer !== null) { + $loop->cancelTimer($timer); + } + + try { + $response = $parser->parseMessage($data); + } catch (\Exception $e) { + $deferred->reject($e); + return; + } + + if ($response->header->isTruncated()) { + $deferred->reject(new \RuntimeException('DNS query for ' . $name . ' failed: The server returned a truncated result for a UDP query, but retrying via TCP is currently not supported')); + return; + } + + $deferred->resolve($response); + }); + $conn->write($queryData); + + return $deferred->promise(); + } + + /** + * @deprecated unused, exists for BC only + */ + protected function generateId() + { + return mt_rand(0, 0xffff); + } + + /** + * @param string $nameserver + * @param string $transport + * @return \React\Stream\DuplexStreamInterface + */ + protected function createConnection($nameserver, $transport) + { + $fd = @stream_socket_client("$transport://$nameserver", $errno, $errstr, 0, STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT); + if ($fd === false) { + throw new \RuntimeException('Unable to connect to DNS server: ' . $errstr, $errno); + } + + // Instantiate stream instance around this stream resource. + // This ought to be replaced with a datagram socket in the future. + // Temporary work around for Windows 10: buffer whole UDP response + // @coverageIgnoreStart + if (!class_exists('React\Stream\Stream')) { + // prefer DuplexResourceStream as of react/stream v0.7.0 + $conn = new DuplexResourceStream($fd, $this->loop, -1); + } else { + // use legacy Stream class for react/stream < v0.7.0 + $conn = new Stream($fd, $this->loop); + $conn->bufferSize = null; + } + // @coverageIgnoreEnd + + return $conn; + } +} diff --git a/assets/php/vendor/react/dns/src/Query/ExecutorInterface.php b/assets/php/vendor/react/dns/src/Query/ExecutorInterface.php new file mode 100644 index 0000000..2f7a635 --- /dev/null +++ b/assets/php/vendor/react/dns/src/Query/ExecutorInterface.php @@ -0,0 +1,8 @@ +<?php + +namespace React\Dns\Query; + +interface ExecutorInterface +{ + public function query($nameserver, Query $query); +} diff --git a/assets/php/vendor/react/dns/src/Query/HostsFileExecutor.php b/assets/php/vendor/react/dns/src/Query/HostsFileExecutor.php new file mode 100644 index 0000000..0ca58be --- /dev/null +++ b/assets/php/vendor/react/dns/src/Query/HostsFileExecutor.php @@ -0,0 +1,89 @@ +<?php + +namespace React\Dns\Query; + +use React\Dns\Config\HostsFile; +use React\Dns\Model\Message; +use React\Dns\Model\Record; +use React\Promise; + +/** + * Resolves hosts from the givne HostsFile or falls back to another executor + * + * If the host is found in the hosts file, it will not be passed to the actual + * DNS executor. If the host is not found in the hosts file, it will be passed + * to the DNS executor as a fallback. + */ +class HostsFileExecutor implements ExecutorInterface +{ + private $hosts; + private $fallback; + + public function __construct(HostsFile $hosts, ExecutorInterface $fallback) + { + $this->hosts = $hosts; + $this->fallback = $fallback; + } + + public function query($nameserver, Query $query) + { + if ($query->class === Message::CLASS_IN && ($query->type === Message::TYPE_A || $query->type === Message::TYPE_AAAA)) { + // forward lookup for type A or AAAA + $records = array(); + $expectsColon = $query->type === Message::TYPE_AAAA; + foreach ($this->hosts->getIpsForHost($query->name) as $ip) { + // ensure this is an IPv4/IPV6 address according to query type + if ((strpos($ip, ':') !== false) === $expectsColon) { + $records[] = new Record($query->name, $query->type, $query->class, 0, $ip); + } + } + + if ($records) { + return Promise\resolve( + Message::createResponseWithAnswersForQuery($query, $records) + ); + } + } elseif ($query->class === Message::CLASS_IN && $query->type === Message::TYPE_PTR) { + // reverse lookup: extract IPv4 or IPv6 from special `.arpa` domain + $ip = $this->getIpFromHost($query->name); + + if ($ip !== null) { + $records = array(); + foreach ($this->hosts->getHostsForIp($ip) as $host) { + $records[] = new Record($query->name, $query->type, $query->class, 0, $host); + } + + if ($records) { + return Promise\resolve( + Message::createResponseWithAnswersForQuery($query, $records) + ); + } + } + } + + return $this->fallback->query($nameserver, $query); + } + + private function getIpFromHost($host) + { + if (substr($host, -13) === '.in-addr.arpa') { + // IPv4: read as IP and reverse bytes + $ip = @inet_pton(substr($host, 0, -13)); + if ($ip === false || isset($ip[4])) { + return null; + } + + return inet_ntop(strrev($ip)); + } elseif (substr($host, -9) === '.ip6.arpa') { + // IPv6: replace dots, reverse nibbles and interpret as hexadecimal string + $ip = @inet_ntop(pack('H*', strrev(str_replace('.', '', substr($host, 0, -9))))); + if ($ip === false) { + return null; + } + + return $ip; + } else { + return null; + } + } +} diff --git a/assets/php/vendor/react/dns/src/Query/Query.php b/assets/php/vendor/react/dns/src/Query/Query.php new file mode 100644 index 0000000..aef6e05 --- /dev/null +++ b/assets/php/vendor/react/dns/src/Query/Query.php @@ -0,0 +1,19 @@ +<?php + +namespace React\Dns\Query; + +class Query +{ + public $name; + public $type; + public $class; + public $currentTime; + + public function __construct($name, $type, $class, $currentTime) + { + $this->name = $name; + $this->type = $type; + $this->class = $class; + $this->currentTime = $currentTime; + } +} diff --git a/assets/php/vendor/react/dns/src/Query/RecordBag.php b/assets/php/vendor/react/dns/src/Query/RecordBag.php new file mode 100644 index 0000000..358cf5d --- /dev/null +++ b/assets/php/vendor/react/dns/src/Query/RecordBag.php @@ -0,0 +1,27 @@ +<?php + +namespace React\Dns\Query; + +use React\Dns\Model\Message; +use React\Dns\Model\Record; + +class RecordBag +{ + private $records = array(); + + public function set($currentTime, Record $record) + { + $this->records[$record->data] = array($currentTime + $record->ttl, $record); + } + + public function all() + { + return array_values(array_map( + function ($value) { + list($expiresAt, $record) = $value; + return $record; + }, + $this->records + )); + } +} diff --git a/assets/php/vendor/react/dns/src/Query/RecordCache.php b/assets/php/vendor/react/dns/src/Query/RecordCache.php new file mode 100644 index 0000000..b8142d3 --- /dev/null +++ b/assets/php/vendor/react/dns/src/Query/RecordCache.php @@ -0,0 +1,82 @@ +<?php + +namespace React\Dns\Query; + +use React\Cache\CacheInterface; +use React\Dns\Model\Message; +use React\Dns\Model\Record; +use React\Promise; + +class RecordCache +{ + private $cache; + private $expiredAt; + + public function __construct(CacheInterface $cache) + { + $this->cache = $cache; + } + + public function lookup(Query $query) + { + $id = $this->serializeQueryToIdentity($query); + + $expiredAt = $this->expiredAt; + + return $this->cache + ->get($id) + ->then(function ($value) use ($query, $expiredAt) { + $recordBag = unserialize($value); + + if (null !== $expiredAt && $expiredAt <= $query->currentTime) { + return Promise\reject(); + } + + return $recordBag->all(); + }); + } + + public function storeResponseMessage($currentTime, Message $message) + { + foreach ($message->answers as $record) { + $this->storeRecord($currentTime, $record); + } + } + + public function storeRecord($currentTime, Record $record) + { + $id = $this->serializeRecordToIdentity($record); + + $cache = $this->cache; + + $this->cache + ->get($id) + ->then( + function ($value) { + return unserialize($value); + }, + function ($e) { + return new RecordBag(); + } + ) + ->then(function ($recordBag) use ($id, $currentTime, $record, $cache) { + $recordBag->set($currentTime, $record); + $cache->set($id, serialize($recordBag)); + }); + } + + public function expire($currentTime) + { + $this->expiredAt = $currentTime; + } + + public function serializeQueryToIdentity(Query $query) + { + return sprintf('%s:%s:%s', $query->name, $query->type, $query->class); + } + + public function serializeRecordToIdentity(Record $record) + { + return sprintf('%s:%s:%s', $record->name, $record->type, $record->class); + } +} diff --git a/assets/php/vendor/react/dns/src/Query/RetryExecutor.php b/assets/php/vendor/react/dns/src/Query/RetryExecutor.php new file mode 100644 index 0000000..90353e5 --- /dev/null +++ b/assets/php/vendor/react/dns/src/Query/RetryExecutor.php @@ -0,0 +1,44 @@ +<?php + +namespace React\Dns\Query; + +use React\Promise\Deferred; + +class RetryExecutor implements ExecutorInterface +{ + private $executor; + private $retries; + + public function __construct(ExecutorInterface $executor, $retries = 2) + { + $this->executor = $executor; + $this->retries = $retries; + } + + public function query($nameserver, Query $query) + { + return $this->tryQuery($nameserver, $query, $this->retries); + } + + public function tryQuery($nameserver, Query $query, $retries) + { + $that = $this; + $errorback = function ($error) use ($nameserver, $query, $retries, $that) { + if (!$error instanceof TimeoutException) { + throw $error; + } + if (0 >= $retries) { + throw new \RuntimeException( + sprintf("DNS query for %s failed: too many retries", $query->name), + 0, + $error + ); + } + return $that->tryQuery($nameserver, $query, $retries-1); + }; + + return $this->executor + ->query($nameserver, $query) + ->then(null, $errorback); + } +} diff --git a/assets/php/vendor/react/dns/src/Query/TimeoutException.php b/assets/php/vendor/react/dns/src/Query/TimeoutException.php new file mode 100644 index 0000000..90bf806 --- /dev/null +++ b/assets/php/vendor/react/dns/src/Query/TimeoutException.php @@ -0,0 +1,7 @@ +<?php + +namespace React\Dns\Query; + +class TimeoutException extends \Exception +{ +} diff --git a/assets/php/vendor/react/dns/src/Query/TimeoutExecutor.php b/assets/php/vendor/react/dns/src/Query/TimeoutExecutor.php new file mode 100644 index 0000000..6a44888 --- /dev/null +++ b/assets/php/vendor/react/dns/src/Query/TimeoutExecutor.php @@ -0,0 +1,32 @@ +<?php + +namespace React\Dns\Query; + +use React\EventLoop\LoopInterface; +use React\Promise\Deferred; +use React\Promise\CancellablePromiseInterface; +use React\Promise\Timer; + +class TimeoutExecutor implements ExecutorInterface +{ + private $executor; + private $loop; + private $timeout; + + public function __construct(ExecutorInterface $executor, $timeout, LoopInterface $loop) + { + $this->executor = $executor; + $this->loop = $loop; + $this->timeout = $timeout; + } + + public function query($nameserver, Query $query) + { + return Timer\timeout($this->executor->query($nameserver, $query), $this->timeout, $this->loop)->then(null, function ($e) use ($query) { + if ($e instanceof Timer\TimeoutException) { + $e = new TimeoutException(sprintf("DNS query for %s timed out", $query->name), 0, $e); + } + throw $e; + }); + } +} diff --git a/assets/php/vendor/react/dns/src/RecordNotFoundException.php b/assets/php/vendor/react/dns/src/RecordNotFoundException.php new file mode 100644 index 0000000..0028413 --- /dev/null +++ b/assets/php/vendor/react/dns/src/RecordNotFoundException.php @@ -0,0 +1,7 @@ +<?php + +namespace React\Dns; + +class RecordNotFoundException extends \Exception +{ +} diff --git a/assets/php/vendor/react/dns/src/Resolver/Factory.php b/assets/php/vendor/react/dns/src/Resolver/Factory.php new file mode 100644 index 0000000..12a912f --- /dev/null +++ b/assets/php/vendor/react/dns/src/Resolver/Factory.php @@ -0,0 +1,103 @@ +<?php + +namespace React\Dns\Resolver; + +use React\Cache\ArrayCache; +use React\Cache\CacheInterface; +use React\Dns\Config\HostsFile; +use React\Dns\Protocol\Parser; +use React\Dns\Protocol\BinaryDumper; +use React\Dns\Query\CachedExecutor; +use React\Dns\Query\Executor; +use React\Dns\Query\ExecutorInterface; +use React\Dns\Query\HostsFileExecutor; +use React\Dns\Query\RecordCache; +use React\Dns\Query\RetryExecutor; +use React\Dns\Query\TimeoutExecutor; +use React\EventLoop\LoopInterface; + +class Factory +{ + public function create($nameserver, LoopInterface $loop) + { + $nameserver = $this->addPortToServerIfMissing($nameserver); + $executor = $this->decorateHostsFileExecutor($this->createRetryExecutor($loop)); + + return new Resolver($nameserver, $executor); + } + + public function createCached($nameserver, LoopInterface $loop, CacheInterface $cache = null) + { + if (!($cache instanceof CacheInterface)) { + $cache = new ArrayCache(); + } + + $nameserver = $this->addPortToServerIfMissing($nameserver); + $executor = $this->decorateHostsFileExecutor($this->createCachedExecutor($loop, $cache)); + + return new Resolver($nameserver, $executor); + } + + /** + * Tries to load the hosts file and decorates the given executor on success + * + * @param ExecutorInterface $executor + * @return ExecutorInterface + * @codeCoverageIgnore + */ + private function decorateHostsFileExecutor(ExecutorInterface $executor) + { + try { + $executor = new HostsFileExecutor( + HostsFile::loadFromPathBlocking(), + $executor + ); + } catch (\RuntimeException $e) { + // ignore this file if it can not be loaded + } + + // Windows does not store localhost in hosts file by default but handles this internally + // To compensate for this, we explicitly use hard-coded defaults for localhost + if (DIRECTORY_SEPARATOR === '\\') { + $executor = new HostsFileExecutor( + new HostsFile("127.0.0.1 localhost\n::1 localhost"), + $executor + ); + } + + return $executor; + } + + protected function createExecutor(LoopInterface $loop) + { + return new TimeoutExecutor( + new Executor($loop, new Parser(), new BinaryDumper(), null), + 5.0, + $loop + ); + } + + protected function createRetryExecutor(LoopInterface $loop) + { + return new RetryExecutor($this->createExecutor($loop)); + } + + protected function createCachedExecutor(LoopInterface $loop, CacheInterface $cache) + { + return new CachedExecutor($this->createRetryExecutor($loop), new RecordCache($cache)); + } + + protected function addPortToServerIfMissing($nameserver) + { + if (strpos($nameserver, '[') === false && substr_count($nameserver, ':') >= 2) { + // several colons, but not enclosed in square brackets => enclose IPv6 address in square brackets + $nameserver = '[' . $nameserver . ']'; + } + // assume a dummy scheme when checking for the port, otherwise parse_url() fails + if (parse_url('dummy://' . $nameserver, PHP_URL_PORT) === null) { + $nameserver .= ':53'; + } + + return $nameserver; + } +} diff --git a/assets/php/vendor/react/dns/src/Resolver/Resolver.php b/assets/php/vendor/react/dns/src/Resolver/Resolver.php new file mode 100644 index 0000000..4a4983a --- /dev/null +++ b/assets/php/vendor/react/dns/src/Resolver/Resolver.php @@ -0,0 +1,100 @@ +<?php + +namespace React\Dns\Resolver; + +use React\Dns\Query\ExecutorInterface; +use React\Dns\Query\Query; +use React\Dns\RecordNotFoundException; +use React\Dns\Model\Message; + +class Resolver +{ + private $nameserver; + private $executor; + + public function __construct($nameserver, ExecutorInterface $executor) + { + $this->nameserver = $nameserver; + $this->executor = $executor; + } + + public function resolve($domain) + { + $query = new Query($domain, Message::TYPE_A, Message::CLASS_IN, time()); + $that = $this; + + return $this->executor + ->query($this->nameserver, $query) + ->then(function (Message $response) use ($query, $that) { + return $that->extractAddress($query, $response); + }); + } + + public function extractAddress(Query $query, Message $response) + { + $answers = $response->answers; + + $addresses = $this->resolveAliases($answers, $query->name); + + if (0 === count($addresses)) { + $message = 'DNS Request did not return valid answer.'; + throw new RecordNotFoundException($message); + } + + $address = $addresses[array_rand($addresses)]; + return $address; + } + + public function resolveAliases(array $answers, $name) + { + $named = $this->filterByName($answers, $name); + $aRecords = $this->filterByType($named, Message::TYPE_A); + $cnameRecords = $this->filterByType($named, Message::TYPE_CNAME); + + if ($aRecords) { + return $this->mapRecordData($aRecords); + } + + if ($cnameRecords) { + $aRecords = array(); + + $cnames = $this->mapRecordData($cnameRecords); + foreach ($cnames as $cname) { + $targets = $this->filterByName($answers, $cname); + $aRecords = array_merge( + $aRecords, + $this->resolveAliases($answers, $cname) + ); + } + + return $aRecords; + } + + return array(); + } + + private function filterByName(array $answers, $name) + { + return $this->filterByField($answers, 'name', $name); + } + + private function filterByType(array $answers, $type) + { + return $this->filterByField($answers, 'type', $type); + } + + private function filterByField(array $answers, $field, $value) + { + $value = strtolower($value); + return array_filter($answers, function ($answer) use ($field, $value) { + return $value === strtolower($answer->$field); + }); + } + + private function mapRecordData(array $records) + { + return array_map(function ($record) { + return $record->data; + }, $records); + } +} diff --git a/assets/php/vendor/react/dns/tests/CallableStub.php b/assets/php/vendor/react/dns/tests/CallableStub.php new file mode 100644 index 0000000..a34a263 --- /dev/null +++ b/assets/php/vendor/react/dns/tests/CallableStub.php @@ -0,0 +1,10 @@ +<?php + +namespace React\Tests\Dns; + +class CallableStub +{ + public function __invoke() + { + } +} diff --git a/assets/php/vendor/react/dns/tests/Config/ConfigTest.php b/assets/php/vendor/react/dns/tests/Config/ConfigTest.php new file mode 100644 index 0000000..8020408 --- /dev/null +++ b/assets/php/vendor/react/dns/tests/Config/ConfigTest.php @@ -0,0 +1,189 @@ +<?php + +namespace React\Tests\Dns\Config; + +use React\Tests\Dns\TestCase; +use React\Dns\Config\Config; + +class ConfigTest extends TestCase +{ + public function testLoadsSystemDefault() + { + $config = Config::loadSystemConfigBlocking(); + + $this->assertInstanceOf('React\Dns\Config\Config', $config); + } + + public function testLoadsDefaultPath() + { + if (DIRECTORY_SEPARATOR === '\\') { + $this->markTestSkipped('Not supported on Windows'); + } + + $config = Config::loadResolvConfBlocking(); + + $this->assertInstanceOf('React\Dns\Config\Config', $config); + } + + public function testLoadsFromExplicitPath() + { + $config = Config::loadResolvConfBlocking(__DIR__ . '/../Fixtures/etc/resolv.conf'); + + $this->assertEquals(array('8.8.8.8'), $config->nameservers); + } + + /** + * @expectedException RuntimeException + */ + public function testLoadThrowsWhenPathIsInvalid() + { + Config::loadResolvConfBlocking(__DIR__ . '/invalid.conf'); + } + + public function testParsesSingleEntryFile() + { + $contents = 'nameserver 8.8.8.8'; + $expected = array('8.8.8.8'); + + $config = Config::loadResolvConfBlocking('data://text/plain;base64,' . base64_encode($contents)); + $this->assertEquals($expected, $config->nameservers); + } + + public function testParsesNameserverEntriesFromAverageFileCorrectly() + { + $contents = '# +# Mac OS X Notice +# +# This file is not used by the host name and address resolution +# or the DNS query routing mechanisms used by most processes on +# this Mac OS X system. +# +# This file is automatically generated. +# +domain v.cablecom.net +nameserver 127.0.0.1 +nameserver ::1 +'; + $expected = array('127.0.0.1', '::1'); + + $config = Config::loadResolvConfBlocking('data://text/plain;base64,' . base64_encode($contents)); + $this->assertEquals($expected, $config->nameservers); + } + + public function testParsesEmptyFileWithoutNameserverEntries() + { + $contents = ''; + $expected = array(); + + $config = Config::loadResolvConfBlocking('data://text/plain;base64,'); + $this->assertEquals($expected, $config->nameservers); + } + + public function testParsesFileAndIgnoresCommentsAndInvalidNameserverEntries() + { + $contents = ' +# nameserver 1.2.3.4 +; nameserver 2.3.4.5 + +nameserver 3.4.5.6 # nope +nameserver 4.5.6.7 5.6.7.8 + nameserver 6.7.8.9 +NameServer 7.8.9.10 +'; + $expected = array(); + + $config = Config::loadResolvConfBlocking('data://text/plain;base64,' . base64_encode($contents)); + $this->assertEquals($expected, $config->nameservers); + } + + public function testLoadsFromWmicOnWindows() + { + if (DIRECTORY_SEPARATOR !== '\\') { + $this->markTestSkipped('Only on Windows'); + } + + $config = Config::loadWmicBlocking(); + + $this->assertInstanceOf('React\Dns\Config\Config', $config); + } + + public function testLoadsSingleEntryFromWmicOutput() + { + $contents = ' +Node,DNSServerSearchOrder +ACE, +ACE,{192.168.2.1} +ACE, +'; + $expected = array('192.168.2.1'); + + $config = Config::loadWmicBlocking($this->echoCommand($contents)); + + $this->assertEquals($expected, $config->nameservers); + } + + public function testLoadsEmptyListFromWmicOutput() + { + $contents = ' +Node,DNSServerSearchOrder +ACE, +'; + $expected = array(); + + $config = Config::loadWmicBlocking($this->echoCommand($contents)); + + $this->assertEquals($expected, $config->nameservers); + } + + public function testLoadsSingleEntryForMultipleNicsFromWmicOutput() + { + $contents = ' +Node,DNSServerSearchOrder +ACE, +ACE,{192.168.2.1} +ACE, +ACE,{192.168.2.2} +ACE, +'; + $expected = array('192.168.2.1', '192.168.2.2'); + + $config = Config::loadWmicBlocking($this->echoCommand($contents)); + + $this->assertEquals($expected, $config->nameservers); + } + + public function testLoadsMultipleEntriesForSingleNicWithSemicolonFromWmicOutput() + { + $contents = ' +Node,DNSServerSearchOrder +ACE, +ACE,{192.168.2.1;192.168.2.2} +ACE, +'; + $expected = array('192.168.2.1', '192.168.2.2'); + + $config = Config::loadWmicBlocking($this->echoCommand($contents)); + + $this->assertEquals($expected, $config->nameservers); + } + + public function testLoadsMultipleEntriesForSingleNicWithQuotesFromWmicOutput() + { + $contents = ' +Node,DNSServerSearchOrder +ACE, +ACE,{"192.168.2.1","192.168.2.2"} +ACE, +'; + $expected = array('192.168.2.1', '192.168.2.2'); + + $config = Config::loadWmicBlocking($this->echoCommand($contents)); + + $this->assertEquals($expected, $config->nameservers); + } + + private function echoCommand($output) + { + return 'echo ' . escapeshellarg($output); + } +} diff --git a/assets/php/vendor/react/dns/tests/Config/FilesystemFactoryTest.php b/assets/php/vendor/react/dns/tests/Config/FilesystemFactoryTest.php new file mode 100644 index 0000000..bb9eac7 --- /dev/null +++ b/assets/php/vendor/react/dns/tests/Config/FilesystemFactoryTest.php @@ -0,0 +1,70 @@ +<?php + +namespace React\Test\Dns\Config; + +use PHPUnit\Framework\TestCase; +use React\Dns\Config\FilesystemFactory; + +class FilesystemFactoryTest extends TestCase +{ + /** @test */ + public function parseEtcResolvConfShouldParseCorrectly() + { + $contents = '# +# Mac OS X Notice +# +# This file is not used by the host name and address resolution +# or the DNS query routing mechanisms used by most processes on +# this Mac OS X system. +# +# This file is automatically generated. +# +domain v.cablecom.net +nameserver 127.0.0.1 +nameserver 8.8.8.8 +'; + $expected = array('127.0.0.1', '8.8.8.8'); + + $capturedConfig = null; + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $factory = new FilesystemFactory($loop); + $factory->parseEtcResolvConf($contents)->then(function ($config) use (&$capturedConfig) { + $capturedConfig = $config; + }); + + $this->assertNotNull($capturedConfig); + $this->assertSame($expected, $capturedConfig->nameservers); + } + + /** @test */ + public function createShouldLoadStuffFromFilesystem() + { + $this->markTestIncomplete('Filesystem API is incomplete'); + + $expected = array('8.8.8.8'); + + $triggerListener = null; + $capturedConfig = null; + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop + ->expects($this->once()) + ->method('addReadStream') + ->will($this->returnCallback(function ($stream, $listener) use (&$triggerListener) { + $triggerListener = function () use ($stream, $listener) { + call_user_func($listener, $stream); + }; + })); + + $factory = new FilesystemFactory($loop); + $factory->create(__DIR__.'/../Fixtures/etc/resolv.conf')->then(function ($config) use (&$capturedConfig) { + $capturedConfig = $config; + }); + + $triggerListener(); + + $this->assertNotNull($capturedConfig); + $this->assertSame($expected, $capturedConfig->nameservers); + } +} diff --git a/assets/php/vendor/react/dns/tests/Config/HostsFileTest.php b/assets/php/vendor/react/dns/tests/Config/HostsFileTest.php new file mode 100644 index 0000000..ff74ad2 --- /dev/null +++ b/assets/php/vendor/react/dns/tests/Config/HostsFileTest.php @@ -0,0 +1,170 @@ +<?php + +namespace React\Tests\Dns\Config; + +use React\Tests\Dns\TestCase; +use React\Dns\Config\HostsFile; + +class HostsFileTest extends TestCase +{ + public function testLoadsFromDefaultPath() + { + $hosts = HostsFile::loadFromPathBlocking(); + + $this->assertInstanceOf('React\Dns\Config\HostsFile', $hosts); + } + + public function testDefaultShouldHaveLocalhostMapped() + { + if (DIRECTORY_SEPARATOR === '\\') { + $this->markTestSkipped('Not supported on Windows'); + } + + $hosts = HostsFile::loadFromPathBlocking(); + + $this->assertContains('127.0.0.1', $hosts->getIpsForHost('localhost')); + } + + /** + * @expectedException RuntimeException + */ + public function testLoadThrowsForInvalidPath() + { + HostsFile::loadFromPathBlocking('does/not/exist'); + } + + public function testContainsSingleLocalhostEntry() + { + $hosts = new HostsFile('127.0.0.1 localhost'); + + $this->assertEquals(array('127.0.0.1'), $hosts->getIpsForHost('localhost')); + $this->assertEquals(array(), $hosts->getIpsForHost('example.com')); + } + + public function testNonIpReturnsNothingForInvalidHosts() + { + $hosts = new HostsFile('a b'); + + $this->assertEquals(array(), $hosts->getIpsForHost('a')); + $this->assertEquals(array(), $hosts->getIpsForHost('b')); + } + + public function testIgnoresIpv6ZoneId() + { + $hosts = new HostsFile('fe80::1%lo0 localhost'); + + $this->assertEquals(array('fe80::1'), $hosts->getIpsForHost('localhost')); + } + + public function testSkipsComments() + { + $hosts = new HostsFile('# start' . PHP_EOL .'#127.0.0.1 localhost' . PHP_EOL . '127.0.0.2 localhost # example.com'); + + $this->assertEquals(array('127.0.0.2'), $hosts->getIpsForHost('localhost')); + $this->assertEquals(array(), $hosts->getIpsForHost('example.com')); + } + + public function testContainsSingleLocalhostEntryWithCaseIgnored() + { + $hosts = new HostsFile('127.0.0.1 LocalHost'); + + $this->assertEquals(array('127.0.0.1'), $hosts->getIpsForHost('LOCALHOST')); + } + + public function testEmptyFileContainsNothing() + { + $hosts = new HostsFile(''); + + $this->assertEquals(array(), $hosts->getIpsForHost('example.com')); + } + + public function testSingleEntryWithMultipleNames() + { + $hosts = new HostsFile('127.0.0.1 localhost example.com'); + + $this->assertEquals(array('127.0.0.1'), $hosts->getIpsForHost('example.com')); + $this->assertEquals(array('127.0.0.1'), $hosts->getIpsForHost('localhost')); + } + + public function testMergesEntriesOverMultipleLines() + { + $hosts = new HostsFile("127.0.0.1 localhost\n127.0.0.2 localhost\n127.0.0.3 a localhost b\n127.0.0.4 a localhost"); + + $this->assertEquals(array('127.0.0.1', '127.0.0.2', '127.0.0.3', '127.0.0.4'), $hosts->getIpsForHost('localhost')); + } + + public function testMergesIpv4AndIpv6EntriesOverMultipleLines() + { + $hosts = new HostsFile("127.0.0.1 localhost\n::1 localhost"); + + $this->assertEquals(array('127.0.0.1', '::1'), $hosts->getIpsForHost('localhost')); + } + + public function testReverseLookup() + { + $hosts = new HostsFile('127.0.0.1 localhost'); + + $this->assertEquals(array('localhost'), $hosts->getHostsForIp('127.0.0.1')); + $this->assertEquals(array(), $hosts->getHostsForIp('192.168.1.1')); + } + + public function testReverseSkipsComments() + { + $hosts = new HostsFile("# start\n#127.0.0.1 localhosted\n127.0.0.2\tlocalhost\t# example.com\n\t127.0.0.3\t\texample.org\t\t"); + + $this->assertEquals(array(), $hosts->getHostsForIp('127.0.0.1')); + $this->assertEquals(array('localhost'), $hosts->getHostsForIp('127.0.0.2')); + $this->assertEquals(array('example.org'), $hosts->getHostsForIp('127.0.0.3')); + } + + public function testReverseNonIpReturnsNothing() + { + $hosts = new HostsFile('127.0.0.1 localhost'); + + $this->assertEquals(array(), $hosts->getHostsForIp('localhost')); + $this->assertEquals(array(), $hosts->getHostsForIp('127.0.0.1.1')); + } + + public function testReverseNonIpReturnsNothingForInvalidHosts() + { + $hosts = new HostsFile('a b'); + + $this->assertEquals(array(), $hosts->getHostsForIp('a')); + $this->assertEquals(array(), $hosts->getHostsForIp('b')); + } + + public function testReverseLookupReturnsLowerCaseHost() + { + $hosts = new HostsFile('127.0.0.1 LocalHost'); + + $this->assertEquals(array('localhost'), $hosts->getHostsForIp('127.0.0.1')); + } + + public function testReverseLookupChecksNormalizedIpv6() + { + $hosts = new HostsFile('FE80::00a1 localhost'); + + $this->assertEquals(array('localhost'), $hosts->getHostsForIp('fe80::A1')); + } + + public function testReverseLookupIgnoresIpv6ZoneId() + { + $hosts = new HostsFile('fe80::1%lo0 localhost'); + + $this->assertEquals(array('localhost'), $hosts->getHostsForIp('fe80::1')); + } + + public function testReverseLookupReturnsMultipleHostsOverSingleLine() + { + $hosts = new HostsFile("::1 ip6-localhost ip6-loopback"); + + $this->assertEquals(array('ip6-localhost', 'ip6-loopback'), $hosts->getHostsForIp('::1')); + } + + public function testReverseLookupReturnsMultipleHostsOverMultipleLines() + { + $hosts = new HostsFile("::1 ip6-localhost\n::1 ip6-loopback"); + + $this->assertEquals(array('ip6-localhost', 'ip6-loopback'), $hosts->getHostsForIp('::1')); + } +} diff --git a/assets/php/vendor/react/dns/tests/Fixtures/etc/resolv.conf b/assets/php/vendor/react/dns/tests/Fixtures/etc/resolv.conf new file mode 100644 index 0000000..cae093a --- /dev/null +++ b/assets/php/vendor/react/dns/tests/Fixtures/etc/resolv.conf @@ -0,0 +1 @@ +nameserver 8.8.8.8 diff --git a/assets/php/vendor/react/dns/tests/FunctionalResolverTest.php b/assets/php/vendor/react/dns/tests/FunctionalResolverTest.php new file mode 100644 index 0000000..0807e86 --- /dev/null +++ b/assets/php/vendor/react/dns/tests/FunctionalResolverTest.php @@ -0,0 +1,71 @@ +<?php + +namespace React\Tests\Dns; + +use React\Tests\Dns\TestCase; +use React\EventLoop\Factory as LoopFactory; +use React\Dns\Resolver\Resolver; +use React\Dns\Resolver\Factory; + +class FunctionalTest extends TestCase +{ + public function setUp() + { + $this->loop = LoopFactory::create(); + + $factory = new Factory(); + $this->resolver = $factory->create('8.8.8.8', $this->loop); + } + + public function testResolveLocalhostResolves() + { + $promise = $this->resolver->resolve('localhost'); + $promise->then($this->expectCallableOnce(), $this->expectCallableNever()); + + $this->loop->run(); + } + + /** + * @group internet + */ + public function testResolveGoogleResolves() + { + $promise = $this->resolver->resolve('google.com'); + $promise->then($this->expectCallableOnce(), $this->expectCallableNever()); + + $this->loop->run(); + } + + /** + * @group internet + */ + public function testResolveInvalidRejects() + { + $promise = $this->resolver->resolve('example.invalid'); + $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); + + $this->loop->run(); + } + + public function testResolveCancelledRejectsImmediately() + { + $promise = $this->resolver->resolve('google.com'); + $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); + $promise->cancel(); + + $time = microtime(true); + $this->loop->run(); + $time = microtime(true) - $time; + + $this->assertLessThan(0.1, $time); + } + + public function testInvalidResolverDoesNotResolveGoogle() + { + $factory = new Factory(); + $this->resolver = $factory->create('255.255.255.255', $this->loop); + + $promise = $this->resolver->resolve('google.com'); + $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); + } +} diff --git a/assets/php/vendor/react/dns/tests/Model/MessageTest.php b/assets/php/vendor/react/dns/tests/Model/MessageTest.php new file mode 100644 index 0000000..53d6b28 --- /dev/null +++ b/assets/php/vendor/react/dns/tests/Model/MessageTest.php @@ -0,0 +1,30 @@ +<?php + +namespace React\Tests\Dns\Model; + +use PHPUnit\Framework\TestCase; +use React\Dns\Query\Query; +use React\Dns\Model\Message; + +class MessageTest extends TestCase +{ + public function testCreateRequestDesiresRecusion() + { + $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); + $request = Message::createRequestForQuery($query); + + $this->assertTrue($request->header->isQuery()); + $this->assertSame(1, $request->header->get('rd')); + } + + public function testCreateResponseWithNoAnswers() + { + $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); + $answers = array(); + $request = Message::createResponseWithAnswersForQuery($query, $answers); + + $this->assertFalse($request->header->isQuery()); + $this->assertTrue($request->header->isResponse()); + $this->assertEquals(0, $request->header->get('anCount')); + } +} diff --git a/assets/php/vendor/react/dns/tests/Protocol/BinaryDumperTest.php b/assets/php/vendor/react/dns/tests/Protocol/BinaryDumperTest.php new file mode 100644 index 0000000..bf60ca9 --- /dev/null +++ b/assets/php/vendor/react/dns/tests/Protocol/BinaryDumperTest.php @@ -0,0 +1,48 @@ +<?php + +namespace React\Tests\Dns\Protocol; + +use PHPUnit\Framework\TestCase; +use React\Dns\Protocol\BinaryDumper; +use React\Dns\Model\Message; + +class BinaryDumperTest extends TestCase +{ + public function testRequestToBinary() + { + $data = ""; + $data .= "72 62 01 00 00 01 00 00 00 00 00 00"; // header + $data .= "04 69 67 6f 72 02 69 6f 00"; // question: igor.io + $data .= "00 01 00 01"; // question: type A, class IN + + $expected = $this->formatHexDump(str_replace(' ', '', $data), 2); + + $request = new Message(); + $request->header->set('id', 0x7262); + $request->header->set('rd', 1); + + $request->questions[] = array( + 'name' => 'igor.io', + 'type' => Message::TYPE_A, + 'class' => Message::CLASS_IN, + ); + + $request->prepare(); + + $dumper = new BinaryDumper(); + $data = $dumper->toBinary($request); + $data = $this->convertBinaryToHexDump($data); + + $this->assertSame($expected, $data); + } + + private function convertBinaryToHexDump($input) + { + return $this->formatHexDump(implode('', unpack('H*', $input))); + } + + private function formatHexDump($input) + { + return implode(' ', str_split($input, 2)); + } +} diff --git a/assets/php/vendor/react/dns/tests/Protocol/ParserTest.php b/assets/php/vendor/react/dns/tests/Protocol/ParserTest.php new file mode 100644 index 0000000..195fad2 --- /dev/null +++ b/assets/php/vendor/react/dns/tests/Protocol/ParserTest.php @@ -0,0 +1,343 @@ +<?php + +namespace React\Tests\Dns\Protocol; + +use PHPUnit\Framework\TestCase; +use React\Dns\Protocol\Parser; +use React\Dns\Model\Message; + +class ParserTest extends TestCase +{ + public function setUp() + { + $this->parser = new Parser(); + } + + /** + * @dataProvider provideConvertTcpDumpToBinary + */ + public function testConvertTcpDumpToBinary($expected, $data) + { + $this->assertSame($expected, $this->convertTcpDumpToBinary($data)); + } + + public function provideConvertTcpDumpToBinary() + { + return array( + array(chr(0x72).chr(0x62), "72 62"), + array(chr(0x72).chr(0x62).chr(0x01).chr(0x00), "72 62 01 00"), + array(chr(0x72).chr(0x62).chr(0x01).chr(0x00).chr(0x00).chr(0x01), "72 62 01 00 00 01"), + array(chr(0x01).chr(0x00).chr(0x01), "01 00 01"), + ); + } + + public function testParseRequest() + { + $data = ""; + $data .= "72 62 01 00 00 01 00 00 00 00 00 00"; // header + $data .= "04 69 67 6f 72 02 69 6f 00"; // question: igor.io + $data .= "00 01 00 01"; // question: type A, class IN + + $data = $this->convertTcpDumpToBinary($data); + + $request = $this->parser->parseMessage($data); + + $header = $request->header; + $this->assertSame(0x7262, $header->get('id')); + $this->assertSame(1, $header->get('qdCount')); + $this->assertSame(0, $header->get('anCount')); + $this->assertSame(0, $header->get('nsCount')); + $this->assertSame(0, $header->get('arCount')); + $this->assertSame(0, $header->get('qr')); + $this->assertSame(Message::OPCODE_QUERY, $header->get('opcode')); + $this->assertSame(0, $header->get('aa')); + $this->assertSame(0, $header->get('tc')); + $this->assertSame(1, $header->get('rd')); + $this->assertSame(0, $header->get('ra')); + $this->assertSame(0, $header->get('z')); + $this->assertSame(Message::RCODE_OK, $header->get('rcode')); + + $this->assertCount(1, $request->questions); + $this->assertSame('igor.io', $request->questions[0]['name']); + $this->assertSame(Message::TYPE_A, $request->questions[0]['type']); + $this->assertSame(Message::CLASS_IN, $request->questions[0]['class']); + } + + public function testParseResponse() + { + $data = ""; + $data .= "72 62 81 80 00 01 00 01 00 00 00 00"; // header + $data .= "04 69 67 6f 72 02 69 6f 00"; // question: igor.io + $data .= "00 01 00 01"; // question: type A, class IN + $data .= "c0 0c"; // answer: offset pointer to igor.io + $data .= "00 01 00 01"; // answer: type A, class IN + $data .= "00 01 51 80"; // answer: ttl 86400 + $data .= "00 04"; // answer: rdlength 4 + $data .= "b2 4f a9 83"; // answer: rdata 178.79.169.131 + + $data = $this->convertTcpDumpToBinary($data); + + $response = $this->parser->parseMessage($data); + + $header = $response->header; + $this->assertSame(0x7262, $header->get('id')); + $this->assertSame(1, $header->get('qdCount')); + $this->assertSame(1, $header->get('anCount')); + $this->assertSame(0, $header->get('nsCount')); + $this->assertSame(0, $header->get('arCount')); + $this->assertSame(1, $header->get('qr')); + $this->assertSame(Message::OPCODE_QUERY, $header->get('opcode')); + $this->assertSame(0, $header->get('aa')); + $this->assertSame(0, $header->get('tc')); + $this->assertSame(1, $header->get('rd')); + $this->assertSame(1, $header->get('ra')); + $this->assertSame(0, $header->get('z')); + $this->assertSame(Message::RCODE_OK, $header->get('rcode')); + + $this->assertCount(1, $response->questions); + $this->assertSame('igor.io', $response->questions[0]['name']); + $this->assertSame(Message::TYPE_A, $response->questions[0]['type']); + $this->assertSame(Message::CLASS_IN, $response->questions[0]['class']); + + $this->assertCount(1, $response->answers); + $this->assertSame('igor.io', $response->answers[0]->name); + $this->assertSame(Message::TYPE_A, $response->answers[0]->type); + $this->assertSame(Message::CLASS_IN, $response->answers[0]->class); + $this->assertSame(86400, $response->answers[0]->ttl); + $this->assertSame('178.79.169.131', $response->answers[0]->data); + } + + public function testParseQuestionWithTwoQuestions() + { + $data = ""; + $data .= "04 69 67 6f 72 02 69 6f 00"; // question: igor.io + $data .= "00 01 00 01"; // question: type A, class IN + $data .= "03 77 77 77 04 69 67 6f 72 02 69 6f 00"; // question: www.igor.io + $data .= "00 01 00 01"; // question: type A, class IN + + $data = $this->convertTcpDumpToBinary($data); + + $request = new Message(); + $request->header->set('qdCount', 2); + $request->data = $data; + + $this->parser->parseQuestion($request); + + $this->assertCount(2, $request->questions); + $this->assertSame('igor.io', $request->questions[0]['name']); + $this->assertSame(Message::TYPE_A, $request->questions[0]['type']); + $this->assertSame(Message::CLASS_IN, $request->questions[0]['class']); + $this->assertSame('www.igor.io', $request->questions[1]['name']); + $this->assertSame(Message::TYPE_A, $request->questions[1]['type']); + $this->assertSame(Message::CLASS_IN, $request->questions[1]['class']); + } + + public function testParseAnswerWithInlineData() + { + $data = ""; + $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io + $data .= "00 01 00 01"; // answer: type A, class IN + $data .= "00 01 51 80"; // answer: ttl 86400 + $data .= "00 04"; // answer: rdlength 4 + $data .= "b2 4f a9 83"; // answer: rdata 178.79.169.131 + + $data = $this->convertTcpDumpToBinary($data); + + $response = new Message(); + $response->header->set('anCount', 1); + $response->data = $data; + + $this->parser->parseAnswer($response); + + $this->assertCount(1, $response->answers); + $this->assertSame('igor.io', $response->answers[0]->name); + $this->assertSame(Message::TYPE_A, $response->answers[0]->type); + $this->assertSame(Message::CLASS_IN, $response->answers[0]->class); + $this->assertSame(86400, $response->answers[0]->ttl); + $this->assertSame('178.79.169.131', $response->answers[0]->data); + } + + public function testParseResponseWithCnameAndOffsetPointers() + { + $data = ""; + $data .= "9e 8d 81 80 00 01 00 01 00 00 00 00"; // header + $data .= "04 6d 61 69 6c 06 67 6f 6f 67 6c 65 03 63 6f 6d 00"; // question: mail.google.com + $data .= "00 05 00 01"; // question: type CNAME, class IN + $data .= "c0 0c"; // answer: offset pointer to mail.google.com + $data .= "00 05 00 01"; // answer: type CNAME, class IN + $data .= "00 00 a8 9c"; // answer: ttl 43164 + $data .= "00 0f"; // answer: rdlength 15 + $data .= "0a 67 6f 6f 67 6c 65 6d 61 69 6c 01 6c"; // answer: rdata googlemail.l. + $data .= "c0 11"; // answer: rdata offset pointer to google.com + + $data = $this->convertTcpDumpToBinary($data); + + $response = $this->parser->parseMessage($data); + + $this->assertCount(1, $response->questions); + $this->assertSame('mail.google.com', $response->questions[0]['name']); + $this->assertSame(Message::TYPE_CNAME, $response->questions[0]['type']); + $this->assertSame(Message::CLASS_IN, $response->questions[0]['class']); + + $this->assertCount(1, $response->answers); + $this->assertSame('mail.google.com', $response->answers[0]->name); + $this->assertSame(Message::TYPE_CNAME, $response->answers[0]->type); + $this->assertSame(Message::CLASS_IN, $response->answers[0]->class); + $this->assertSame(43164, $response->answers[0]->ttl); + $this->assertSame('googlemail.l.google.com', $response->answers[0]->data); + } + + public function testParseAAAAResponse() + { + $data = ""; + $data .= "cd 72 81 80 00 01 00 01 00 00 00 00 06"; // header + $data .= "67 6f 6f 67 6c 65 03 63 6f 6d 00"; // question: google.com + $data .= "00 1c 00 01"; // question: type AAAA, class IN + $data .= "c0 0c"; // answer: offset pointer to google.com + $data .= "00 1c 00 01"; // answer: type AAAA, class IN + $data .= "00 00 01 2b"; // answer: ttl 299 + $data .= "00 10"; // answer: rdlength 16 + $data .= "2a 00 14 50 40 09 08 09 00 00 00 00 00 00 20 0e"; // answer: 2a00:1450:4009:809::200e + + $data = $this->convertTcpDumpToBinary($data); + + $response = $this->parser->parseMessage($data); + + $header = $response->header; + $this->assertSame(0xcd72, $header->get('id')); + $this->assertSame(1, $header->get('qdCount')); + $this->assertSame(1, $header->get('anCount')); + $this->assertSame(0, $header->get('nsCount')); + $this->assertSame(0, $header->get('arCount')); + $this->assertSame(1, $header->get('qr')); + $this->assertSame(Message::OPCODE_QUERY, $header->get('opcode')); + $this->assertSame(0, $header->get('aa')); + $this->assertSame(0, $header->get('tc')); + $this->assertSame(1, $header->get('rd')); + $this->assertSame(1, $header->get('ra')); + $this->assertSame(0, $header->get('z')); + $this->assertSame(Message::RCODE_OK, $header->get('rcode')); + + $this->assertCount(1, $response->questions); + $this->assertSame('google.com', $response->questions[0]['name']); + $this->assertSame(Message::TYPE_AAAA, $response->questions[0]['type']); + $this->assertSame(Message::CLASS_IN, $response->questions[0]['class']); + + $this->assertCount(1, $response->answers); + $this->assertSame('google.com', $response->answers[0]->name); + $this->assertSame(Message::TYPE_AAAA, $response->answers[0]->type); + $this->assertSame(Message::CLASS_IN, $response->answers[0]->class); + $this->assertSame(299, $response->answers[0]->ttl); + $this->assertSame('2a00:1450:4009:809::200e', $response->answers[0]->data); + } + + public function testParseResponseWithTwoAnswers() + { + $data = ""; + $data .= "bc 73 81 80 00 01 00 02 00 00 00 00"; // header + $data .= "02 69 6f 0d 77 68 6f 69 73 2d 73 65 72 76 65 72 73 03 6e 65 74 00"; + // question: io.whois-servers.net + $data .= "00 01 00 01"; // question: type A, class IN + $data .= "c0 0c"; // answer: offset pointer to io.whois-servers.net + $data .= "00 05 00 01"; // answer: type CNAME, class IN + $data .= "00 00 00 29"; // answer: ttl 41 + $data .= "00 0e"; // answer: rdlength 14 + $data .= "05 77 68 6f 69 73 03 6e 69 63 02 69 6f 00"; // answer: rdata whois.nic.io + $data .= "c0 32"; // answer: offset pointer to whois.nic.io + $data .= "00 01 00 01"; // answer: type CNAME, class IN + $data .= "00 00 0d f7"; // answer: ttl 3575 + $data .= "00 04"; // answer: rdlength 4 + $data .= "c1 df 4e 98"; // answer: rdata 193.223.78.152 + + $data = $this->convertTcpDumpToBinary($data); + + $response = $this->parser->parseMessage($data); + + $this->assertCount(1, $response->questions); + $this->assertSame('io.whois-servers.net', $response->questions[0]['name']); + $this->assertSame(Message::TYPE_A, $response->questions[0]['type']); + $this->assertSame(Message::CLASS_IN, $response->questions[0]['class']); + + $this->assertCount(2, $response->answers); + + $this->assertSame('io.whois-servers.net', $response->answers[0]->name); + $this->assertSame(Message::TYPE_CNAME, $response->answers[0]->type); + $this->assertSame(Message::CLASS_IN, $response->answers[0]->class); + $this->assertSame(41, $response->answers[0]->ttl); + $this->assertSame('whois.nic.io', $response->answers[0]->data); + + $this->assertSame('whois.nic.io', $response->answers[1]->name); + $this->assertSame(Message::TYPE_A, $response->answers[1]->type); + $this->assertSame(Message::CLASS_IN, $response->answers[1]->class); + $this->assertSame(3575, $response->answers[1]->ttl); + $this->assertSame('193.223.78.152', $response->answers[1]->data); + } + + public function testParsePTRResponse() + { + $data = ""; + $data .= "5d d8 81 80 00 01 00 01 00 00 00 00"; // header + $data .= "01 34 01 34 01 38 01 38 07 69 6e"; // question: 4.4.8.8.in-addr.arpa + $data .= "2d 61 64 64 72 04 61 72 70 61 00"; // question (continued) + $data .= "00 0c 00 01"; // question: type PTR, class IN + $data .= "c0 0c"; // answer: offset pointer to rdata + $data .= "00 0c 00 01"; // answer: type PTR, class IN + $data .= "00 01 51 7f"; // answer: ttl 86399 + $data .= "00 20"; // answer: rdlength 32 + $data .= "13 67 6f 6f 67 6c 65 2d 70 75 62 6c 69 63 2d 64"; // answer: rdata google-public-dns-b.google.com. + $data .= "6e 73 2d 62 06 67 6f 6f 67 6c 65 03 63 6f 6d 00"; + + $data = $this->convertTcpDumpToBinary($data); + + $response = $this->parser->parseMessage($data); + + $header = $response->header; + $this->assertSame(0x5dd8, $header->get('id')); + $this->assertSame(1, $header->get('qdCount')); + $this->assertSame(1, $header->get('anCount')); + $this->assertSame(0, $header->get('nsCount')); + $this->assertSame(0, $header->get('arCount')); + $this->assertSame(1, $header->get('qr')); + $this->assertSame(Message::OPCODE_QUERY, $header->get('opcode')); + $this->assertSame(0, $header->get('aa')); + $this->assertSame(0, $header->get('tc')); + $this->assertSame(1, $header->get('rd')); + $this->assertSame(1, $header->get('ra')); + $this->assertSame(0, $header->get('z')); + $this->assertSame(Message::RCODE_OK, $header->get('rcode')); + + $this->assertCount(1, $response->questions); + $this->assertSame('4.4.8.8.in-addr.arpa', $response->questions[0]['name']); + $this->assertSame(Message::TYPE_PTR, $response->questions[0]['type']); + $this->assertSame(Message::CLASS_IN, $response->questions[0]['class']); + + $this->assertCount(1, $response->answers); + $this->assertSame('4.4.8.8.in-addr.arpa', $response->answers[0]->name); + $this->assertSame(Message::TYPE_PTR, $response->answers[0]->type); + $this->assertSame(Message::CLASS_IN, $response->answers[0]->class); + $this->assertSame(86399, $response->answers[0]->ttl); + $this->assertSame('google-public-dns-b.google.com', $response->answers[0]->data); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testParseIncomplete() + { + $data = ""; + $data .= "72 62 01 00 00 01 00 00 00 00 00 00"; // header + $data .= "04 69 67 6f 72 02 69 6f 00"; // question: igor.io + //$data .= "00 01 00 01"; // question: type A, class IN + + $data = $this->convertTcpDumpToBinary($data); + + $this->parser->parseMessage($data); + } + + private function convertTcpDumpToBinary($input) + { + // sudo ngrep -d en1 -x port 53 + + return pack('H*', str_replace(' ', '', $input)); + } +} diff --git a/assets/php/vendor/react/dns/tests/Query/CachedExecutorTest.php b/assets/php/vendor/react/dns/tests/Query/CachedExecutorTest.php new file mode 100644 index 0000000..d08ed05 --- /dev/null +++ b/assets/php/vendor/react/dns/tests/Query/CachedExecutorTest.php @@ -0,0 +1,100 @@ +<?php + +namespace React\Tests\Dns\Query; + +use React\Tests\Dns\TestCase; +use React\Dns\Query\CachedExecutor; +use React\Dns\Query\Query; +use React\Dns\Model\Message; +use React\Dns\Model\Record; +use React\Promise; + +class CachedExecutorTest extends TestCase +{ + /** + * @covers React\Dns\Query\CachedExecutor + * @test + */ + public function queryShouldDelegateToDecoratedExecutor() + { + $executor = $this->createExecutorMock(); + $executor + ->expects($this->once()) + ->method('query') + ->with('8.8.8.8', $this->isInstanceOf('React\Dns\Query\Query')) + ->will($this->returnValue($this->createPromiseMock())); + + $cache = $this->getMockBuilder('React\Dns\Query\RecordCache') + ->disableOriginalConstructor() + ->getMock(); + $cache + ->expects($this->once()) + ->method('lookup') + ->will($this->returnValue(Promise\reject())); + $cachedExecutor = new CachedExecutor($executor, $cache); + + $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); + $cachedExecutor->query('8.8.8.8', $query); + } + + /** + * @covers React\Dns\Query\CachedExecutor + * @test + */ + public function callingQueryTwiceShouldUseCachedResult() + { + $cachedRecords = array(new Record('igor.io', Message::TYPE_A, Message::CLASS_IN)); + + $executor = $this->createExecutorMock(); + $executor + ->expects($this->once()) + ->method('query') + ->will($this->callQueryCallbackWithAddress('178.79.169.131')); + + $cache = $this->getMockBuilder('React\Dns\Query\RecordCache') + ->disableOriginalConstructor() + ->getMock(); + $cache + ->expects($this->at(0)) + ->method('lookup') + ->with($this->isInstanceOf('React\Dns\Query\Query')) + ->will($this->returnValue(Promise\reject())); + $cache + ->expects($this->at(1)) + ->method('storeResponseMessage') + ->with($this->isType('integer'), $this->isInstanceOf('React\Dns\Model\Message')); + $cache + ->expects($this->at(2)) + ->method('lookup') + ->with($this->isInstanceOf('React\Dns\Query\Query')) + ->will($this->returnValue(Promise\resolve($cachedRecords))); + + $cachedExecutor = new CachedExecutor($executor, $cache); + + $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); + $cachedExecutor->query('8.8.8.8', $query, function () {}, function () {}); + $cachedExecutor->query('8.8.8.8', $query, function () {}, function () {}); + } + + private function callQueryCallbackWithAddress($address) + { + return $this->returnCallback(function ($nameserver, $query) use ($address) { + $response = new Message(); + $response->header->set('qr', 1); + $response->questions[] = new Record($query->name, $query->type, $query->class); + $response->answers[] = new Record($query->name, $query->type, $query->class, 3600, $address); + + return Promise\resolve($response); + }); + } + + private function createExecutorMock() + { + return $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock(); + } + + private function createPromiseMock() + { + return $this->getMockBuilder('React\Promise\PromiseInterface')->getMock(); + } +} diff --git a/assets/php/vendor/react/dns/tests/Query/ExecutorTest.php b/assets/php/vendor/react/dns/tests/Query/ExecutorTest.php new file mode 100644 index 0000000..0d7ac1d --- /dev/null +++ b/assets/php/vendor/react/dns/tests/Query/ExecutorTest.php @@ -0,0 +1,308 @@ +<?php + +namespace React\Tests\Dns\Query; + +use Clue\React\Block; +use React\Dns\Query\Executor; +use React\Dns\Query\Query; +use React\Dns\Model\Message; +use React\Dns\Model\Record; +use React\Dns\Protocol\BinaryDumper; +use React\Tests\Dns\TestCase; + +class ExecutorTest extends TestCase +{ + private $loop; + private $parser; + private $dumper; + private $executor; + + public function setUp() + { + $this->loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $this->parser = $this->getMockBuilder('React\Dns\Protocol\Parser')->getMock(); + $this->dumper = new BinaryDumper(); + + $this->executor = new Executor($this->loop, $this->parser, $this->dumper); + } + + /** @test */ + public function queryShouldCreateUdpRequest() + { + $timer = $this->createTimerMock(); + $this->loop + ->expects($this->any()) + ->method('addTimer') + ->will($this->returnValue($timer)); + + $this->executor = $this->createExecutorMock(); + $this->executor + ->expects($this->once()) + ->method('createConnection') + ->with('8.8.8.8:53', 'udp') + ->will($this->returnNewConnectionMock(false)); + + $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); + $this->executor->query('8.8.8.8:53', $query); + } + + /** @test */ + public function resolveShouldRejectIfRequestIsLargerThan512Bytes() + { + $query = new Query(str_repeat('a', 512).'.igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); + $promise = $this->executor->query('8.8.8.8:53', $query); + + $this->setExpectedException('RuntimeException', 'DNS query for ' . $query->name . ' failed: Requested transport "tcp" not available, only UDP is supported in this version'); + Block\await($promise, $this->loop); + } + + /** @test */ + public function resolveShouldCloseConnectionWhenCancelled() + { + $conn = $this->createConnectionMock(false); + $conn->expects($this->once())->method('close'); + + $timer = $this->createTimerMock(); + $this->loop + ->expects($this->any()) + ->method('addTimer') + ->will($this->returnValue($timer)); + + $this->executor = $this->createExecutorMock(); + $this->executor + ->expects($this->once()) + ->method('createConnection') + ->with('8.8.8.8:53', 'udp') + ->will($this->returnValue($conn)); + + $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); + $promise = $this->executor->query('8.8.8.8:53', $query); + + $promise->cancel(); + + $this->setExpectedException('React\Dns\Query\CancellationException', 'DNS query for igor.io has been cancelled'); + Block\await($promise, $this->loop); + } + + /** @test */ + public function resolveShouldNotStartOrCancelTimerWhenCancelledWithTimeoutIsNull() + { + $this->loop + ->expects($this->never()) + ->method('addTimer'); + + $this->executor = new Executor($this->loop, $this->parser, $this->dumper, null); + + $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); + $promise = $this->executor->query('127.0.0.1:53', $query); + + $promise->cancel(); + + $this->setExpectedException('React\Dns\Query\CancellationException', 'DNS query for igor.io has been cancelled'); + Block\await($promise, $this->loop); + } + + /** @test */ + public function resolveShouldRejectIfResponseIsTruncated() + { + $timer = $this->createTimerMock(); + + $this->loop + ->expects($this->any()) + ->method('addTimer') + ->will($this->returnValue($timer)); + + $this->parser + ->expects($this->once()) + ->method('parseMessage') + ->will($this->returnTruncatedResponse()); + + $this->executor = $this->createExecutorMock(); + $this->executor + ->expects($this->once()) + ->method('createConnection') + ->with('8.8.8.8:53', 'udp') + ->will($this->returnNewConnectionMock()); + + $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); + $this->executor->query('8.8.8.8:53', $query); + } + + /** @test */ + public function resolveShouldFailIfUdpThrow() + { + $this->loop + ->expects($this->never()) + ->method('addTimer'); + + $this->parser + ->expects($this->never()) + ->method('parseMessage'); + + $this->executor = $this->createExecutorMock(); + $this->executor + ->expects($this->once()) + ->method('createConnection') + ->with('8.8.8.8:53', 'udp') + ->will($this->throwException(new \Exception('Nope'))); + + $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); + $promise = $this->executor->query('8.8.8.8:53', $query); + + $this->setExpectedException('RuntimeException', 'DNS query for igor.io failed: Nope'); + Block\await($promise, $this->loop); + } + + /** @test */ + public function resolveShouldCancelTimerWhenFullResponseIsReceived() + { + $conn = $this->createConnectionMock(); + + $this->parser + ->expects($this->once()) + ->method('parseMessage') + ->will($this->returnStandardResponse()); + + $this->executor = $this->createExecutorMock(); + $this->executor + ->expects($this->at(0)) + ->method('createConnection') + ->with('8.8.8.8:53', 'udp') + ->will($this->returnNewConnectionMock()); + + + $timer = $this->createTimerMock(); + + $this->loop + ->expects($this->once()) + ->method('addTimer') + ->with(5, $this->isInstanceOf('Closure')) + ->will($this->returnValue($timer)); + + $this->loop + ->expects($this->once()) + ->method('cancelTimer') + ->with($timer); + + $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); + $this->executor->query('8.8.8.8:53', $query); + } + + /** @test */ + public function resolveShouldCloseConnectionOnTimeout() + { + $this->executor = $this->createExecutorMock(); + $this->executor + ->expects($this->at(0)) + ->method('createConnection') + ->with('8.8.8.8:53', 'udp') + ->will($this->returnNewConnectionMock(false)); + + $timer = $this->createTimerMock(); + + $this->loop + ->expects($this->never()) + ->method('cancelTimer'); + + $this->loop + ->expects($this->once()) + ->method('addTimer') + ->with(5, $this->isInstanceOf('Closure')) + ->will($this->returnCallback(function ($time, $callback) use (&$timerCallback, $timer) { + $timerCallback = $callback; + return $timer; + })); + + $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); + $promise = $this->executor->query('8.8.8.8:53', $query); + + $this->assertNotNull($timerCallback); + $timerCallback(); + + $this->setExpectedException('React\Dns\Query\TimeoutException', 'DNS query for igor.io timed out'); + Block\await($promise, $this->loop); + } + + private function returnStandardResponse() + { + $that = $this; + $callback = function ($data) use ($that) { + $response = new Message(); + $that->convertMessageToStandardResponse($response); + return $response; + }; + + return $this->returnCallback($callback); + } + + private function returnTruncatedResponse() + { + $that = $this; + $callback = function ($data) use ($that) { + $response = new Message(); + $that->convertMessageToTruncatedResponse($response); + return $response; + }; + + return $this->returnCallback($callback); + } + + public function convertMessageToStandardResponse(Message $response) + { + $response->header->set('qr', 1); + $response->questions[] = new Record('igor.io', Message::TYPE_A, Message::CLASS_IN); + $response->answers[] = new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131'); + $response->prepare(); + + return $response; + } + + public function convertMessageToTruncatedResponse(Message $response) + { + $this->convertMessageToStandardResponse($response); + $response->header->set('tc', 1); + $response->prepare(); + + return $response; + } + + private function returnNewConnectionMock($emitData = true) + { + $conn = $this->createConnectionMock($emitData); + + $callback = function () use ($conn) { + return $conn; + }; + + return $this->returnCallback($callback); + } + + private function createConnectionMock($emitData = true) + { + $conn = $this->getMockBuilder('React\Stream\DuplexStreamInterface')->getMock(); + $conn + ->expects($this->any()) + ->method('on') + ->with('data', $this->isInstanceOf('Closure')) + ->will($this->returnCallback(function ($name, $callback) use ($emitData) { + $emitData && $callback(null); + })); + + return $conn; + } + + private function createTimerMock() + { + return $this->getMockBuilder( + interface_exists('React\EventLoop\TimerInterface') ? 'React\EventLoop\TimerInterface' : 'React\EventLoop\Timer\TimerInterface' + )->getMock(); + } + + private function createExecutorMock() + { + return $this->getMockBuilder('React\Dns\Query\Executor') + ->setConstructorArgs(array($this->loop, $this->parser, $this->dumper)) + ->setMethods(array('createConnection')) + ->getMock(); + } +} diff --git a/assets/php/vendor/react/dns/tests/Query/HostsFileExecutorTest.php b/assets/php/vendor/react/dns/tests/Query/HostsFileExecutorTest.php new file mode 100644 index 0000000..70d877e --- /dev/null +++ b/assets/php/vendor/react/dns/tests/Query/HostsFileExecutorTest.php @@ -0,0 +1,126 @@ +<?php + +namespace React\Tests\Dns\Query; + +use React\Tests\Dns\TestCase; +use React\Dns\Query\HostsFileExecutor; +use React\Dns\Query\Query; +use React\Dns\Model\Message; + +class HostsFileExecutorTest extends TestCase +{ + private $hosts; + private $fallback; + private $executor; + + public function setUp() + { + $this->hosts = $this->getMockBuilder('React\Dns\Config\HostsFile')->disableOriginalConstructor()->getMock(); + $this->fallback = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock(); + $this->executor = new HostsFileExecutor($this->hosts, $this->fallback); + } + + public function testDoesNotTryToGetIpsForMxQuery() + { + $this->hosts->expects($this->never())->method('getIpsForHost'); + $this->fallback->expects($this->once())->method('query'); + + $this->executor->query('8.8.8.8', new Query('google.com', Message::TYPE_MX, Message::CLASS_IN, 0)); + } + + public function testFallsBackIfNoIpsWereFound() + { + $this->hosts->expects($this->once())->method('getIpsForHost')->willReturn(array()); + $this->fallback->expects($this->once())->method('query'); + + $this->executor->query('8.8.8.8', new Query('google.com', Message::TYPE_A, Message::CLASS_IN, 0)); + } + + public function testReturnsResponseMessageIfIpsWereFound() + { + $this->hosts->expects($this->once())->method('getIpsForHost')->willReturn(array('127.0.0.1')); + $this->fallback->expects($this->never())->method('query'); + + $ret = $this->executor->query('8.8.8.8', new Query('google.com', Message::TYPE_A, Message::CLASS_IN, 0)); + } + + public function testFallsBackIfNoIpv4Matches() + { + $this->hosts->expects($this->once())->method('getIpsForHost')->willReturn(array('::1')); + $this->fallback->expects($this->once())->method('query'); + + $ret = $this->executor->query('8.8.8.8', new Query('google.com', Message::TYPE_A, Message::CLASS_IN, 0)); + } + + public function testReturnsResponseMessageIfIpv6AddressesWereFound() + { + $this->hosts->expects($this->once())->method('getIpsForHost')->willReturn(array('::1')); + $this->fallback->expects($this->never())->method('query'); + + $ret = $this->executor->query('8.8.8.8', new Query('google.com', Message::TYPE_AAAA, Message::CLASS_IN, 0)); + } + + public function testFallsBackIfNoIpv6Matches() + { + $this->hosts->expects($this->once())->method('getIpsForHost')->willReturn(array('127.0.0.1')); + $this->fallback->expects($this->once())->method('query'); + + $ret = $this->executor->query('8.8.8.8', new Query('google.com', Message::TYPE_AAAA, Message::CLASS_IN, 0)); + } + + public function testDoesReturnReverseIpv4Lookup() + { + $this->hosts->expects($this->once())->method('getHostsForIp')->with('127.0.0.1')->willReturn(array('localhost')); + $this->fallback->expects($this->never())->method('query'); + + $this->executor->query('8.8.8.8', new Query('1.0.0.127.in-addr.arpa', Message::TYPE_PTR, Message::CLASS_IN, 0)); + } + + public function testFallsBackIfNoReverseIpv4Matches() + { + $this->hosts->expects($this->once())->method('getHostsForIp')->with('127.0.0.1')->willReturn(array()); + $this->fallback->expects($this->once())->method('query'); + + $this->executor->query('8.8.8.8', new Query('1.0.0.127.in-addr.arpa', Message::TYPE_PTR, Message::CLASS_IN, 0)); + } + + public function testDoesReturnReverseIpv6Lookup() + { + $this->hosts->expects($this->once())->method('getHostsForIp')->with('2a02:2e0:3fe:100::6')->willReturn(array('ip6-localhost')); + $this->fallback->expects($this->never())->method('query'); + + $this->executor->query('8.8.8.8', new Query('6.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.1.0.e.f.3.0.0.e.2.0.2.0.a.2.ip6.arpa', Message::TYPE_PTR, Message::CLASS_IN, 0)); + } + + public function testFallsBackForInvalidAddress() + { + $this->hosts->expects($this->never())->method('getHostsForIp'); + $this->fallback->expects($this->once())->method('query'); + + $this->executor->query('8.8.8.8', new Query('example.com', Message::TYPE_PTR, Message::CLASS_IN, 0)); + } + + public function testReverseFallsBackForInvalidIpv4Address() + { + $this->hosts->expects($this->never())->method('getHostsForIp'); + $this->fallback->expects($this->once())->method('query'); + + $this->executor->query('8.8.8.8', new Query('::1.in-addr.arpa', Message::TYPE_PTR, Message::CLASS_IN, 0)); + } + + public function testReverseFallsBackForInvalidLengthIpv6Address() + { + $this->hosts->expects($this->never())->method('getHostsForIp'); + $this->fallback->expects($this->once())->method('query'); + + $this->executor->query('8.8.8.8', new Query('abcd.ip6.arpa', Message::TYPE_PTR, Message::CLASS_IN, 0)); + } + + public function testReverseFallsBackForInvalidHexIpv6Address() + { + $this->hosts->expects($this->never())->method('getHostsForIp'); + $this->fallback->expects($this->once())->method('query'); + + $this->executor->query('8.8.8.8', new Query('zZz.ip6.arpa', Message::TYPE_PTR, Message::CLASS_IN, 0)); + } +} diff --git a/assets/php/vendor/react/dns/tests/Query/RecordBagTest.php b/assets/php/vendor/react/dns/tests/Query/RecordBagTest.php new file mode 100644 index 0000000..83b8934 --- /dev/null +++ b/assets/php/vendor/react/dns/tests/Query/RecordBagTest.php @@ -0,0 +1,64 @@ +<?php + +namespace React\Tests\Dns\Query; + +use PHPUnit\Framework\TestCase; +use React\Dns\Query\RecordBag; +use React\Dns\Model\Message; +use React\Dns\Model\Record; + +class RecordBagTest extends TestCase +{ + /** + * @covers React\Dns\Query\RecordBag + * @test + */ + public function emptyBagShouldBeEmpty() + { + $recordBag = new RecordBag(); + + $this->assertSame(array(), $recordBag->all()); + } + + /** + * @covers React\Dns\Query\RecordBag + * @test + */ + public function setShouldSetTheValue() + { + $currentTime = 1345656451; + + $recordBag = new RecordBag(); + $recordBag->set($currentTime, new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600)); + + $records = $recordBag->all(); + $this->assertCount(1, $records); + $this->assertSame('igor.io', $records[0]->name); + $this->assertSame(Message::TYPE_A, $records[0]->type); + $this->assertSame(Message::CLASS_IN, $records[0]->class); + } + + /** + * @covers React\Dns\Query\RecordBag + * @test + */ + public function setShouldSetManyValues() + { + $currentTime = 1345656451; + + $recordBag = new RecordBag(); + $recordBag->set($currentTime, new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131')); + $recordBag->set($currentTime, new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.132')); + + $records = $recordBag->all(); + $this->assertCount(2, $records); + $this->assertSame('igor.io', $records[0]->name); + $this->assertSame(Message::TYPE_A, $records[0]->type); + $this->assertSame(Message::CLASS_IN, $records[0]->class); + $this->assertSame('178.79.169.131', $records[0]->data); + $this->assertSame('igor.io', $records[1]->name); + $this->assertSame(Message::TYPE_A, $records[1]->type); + $this->assertSame(Message::CLASS_IN, $records[1]->class); + $this->assertSame('178.79.169.132', $records[1]->data); + } +} diff --git a/assets/php/vendor/react/dns/tests/Query/RecordCacheTest.php b/assets/php/vendor/react/dns/tests/Query/RecordCacheTest.php new file mode 100644 index 0000000..399fbe8 --- /dev/null +++ b/assets/php/vendor/react/dns/tests/Query/RecordCacheTest.php @@ -0,0 +1,123 @@ +<?php + +namespace React\Tests\Dns\Query; + +use PHPUnit\Framework\TestCase; +use React\Cache\ArrayCache; +use React\Dns\Model\Message; +use React\Dns\Model\Record; +use React\Dns\Query\RecordCache; +use React\Dns\Query\Query; +use React\Promise\PromiseInterface; + +class RecordCacheTest extends TestCase +{ + /** + * @covers React\Dns\Query\RecordCache + * @test + */ + public function lookupOnEmptyCacheShouldReturnNull() + { + $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); + + $cache = new RecordCache(new ArrayCache()); + $promise = $cache->lookup($query); + + $this->assertInstanceOf('React\Promise\RejectedPromise', $promise); + } + + /** + * @covers React\Dns\Query\RecordCache + * @test + */ + public function storeRecordShouldMakeLookupSucceed() + { + $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); + + $cache = new RecordCache(new ArrayCache()); + $cache->storeRecord($query->currentTime, new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131')); + $promise = $cache->lookup($query); + + $this->assertInstanceOf('React\Promise\FulfilledPromise', $promise); + $cachedRecords = $this->getPromiseValue($promise); + + $this->assertCount(1, $cachedRecords); + $this->assertSame('178.79.169.131', $cachedRecords[0]->data); + } + + /** + * @covers React\Dns\Query\RecordCache + * @test + */ + public function storeTwoRecordsShouldReturnBoth() + { + $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); + + $cache = new RecordCache(new ArrayCache()); + $cache->storeRecord($query->currentTime, new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131')); + $cache->storeRecord($query->currentTime, new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.132')); + $promise = $cache->lookup($query); + + $this->assertInstanceOf('React\Promise\FulfilledPromise', $promise); + $cachedRecords = $this->getPromiseValue($promise); + + $this->assertCount(2, $cachedRecords); + $this->assertSame('178.79.169.131', $cachedRecords[0]->data); + $this->assertSame('178.79.169.132', $cachedRecords[1]->data); + } + + /** + * @covers React\Dns\Query\RecordCache + * @test + */ + public function storeResponseMessageShouldStoreAllAnswerValues() + { + $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); + + $response = new Message(); + $response->answers[] = new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131'); + $response->answers[] = new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.132'); + $response->prepare(); + + $cache = new RecordCache(new ArrayCache()); + $cache->storeResponseMessage($query->currentTime, $response); + $promise = $cache->lookup($query); + + $this->assertInstanceOf('React\Promise\FulfilledPromise', $promise); + $cachedRecords = $this->getPromiseValue($promise); + + $this->assertCount(2, $cachedRecords); + $this->assertSame('178.79.169.131', $cachedRecords[0]->data); + $this->assertSame('178.79.169.132', $cachedRecords[1]->data); + } + + /** + * @covers React\Dns\Query\RecordCache + * @test + */ + public function expireShouldExpireDeadRecords() + { + $cachedTime = 1345656451; + $currentTime = $cachedTime + 3605; + + $cache = new RecordCache(new ArrayCache()); + $cache->storeRecord($cachedTime, new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131')); + $cache->expire($currentTime); + + $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, $currentTime); + $promise = $cache->lookup($query); + + $this->assertInstanceOf('React\Promise\RejectedPromise', $promise); + } + + private function getPromiseValue(PromiseInterface $promise) + { + $capturedValue = null; + + $promise->then(function ($value) use (&$capturedValue) { + $capturedValue = $value; + }); + + return $capturedValue; + } +} diff --git a/assets/php/vendor/react/dns/tests/Query/RetryExecutorTest.php b/assets/php/vendor/react/dns/tests/Query/RetryExecutorTest.php new file mode 100644 index 0000000..8950f84 --- /dev/null +++ b/assets/php/vendor/react/dns/tests/Query/RetryExecutorTest.php @@ -0,0 +1,197 @@ +<?php + +namespace React\Tests\Dns\Query; + +use React\Tests\Dns\TestCase; +use React\Dns\Query\RetryExecutor; +use React\Dns\Query\Query; +use React\Dns\Model\Message; +use React\Dns\Query\TimeoutException; +use React\Dns\Model\Record; +use React\Promise; +use React\Promise\Deferred; +use React\Dns\Query\CancellationException; + +class RetryExecutorTest extends TestCase +{ + /** + * @covers React\Dns\Query\RetryExecutor + * @test + */ + public function queryShouldDelegateToDecoratedExecutor() + { + $executor = $this->createExecutorMock(); + $executor + ->expects($this->once()) + ->method('query') + ->with('8.8.8.8', $this->isInstanceOf('React\Dns\Query\Query')) + ->will($this->returnValue($this->expectPromiseOnce())); + + $retryExecutor = new RetryExecutor($executor, 2); + + $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); + $retryExecutor->query('8.8.8.8', $query); + } + + /** + * @covers React\Dns\Query\RetryExecutor + * @test + */ + public function queryShouldRetryQueryOnTimeout() + { + $response = $this->createStandardResponse(); + + $executor = $this->createExecutorMock(); + $executor + ->expects($this->exactly(2)) + ->method('query') + ->with('8.8.8.8', $this->isInstanceOf('React\Dns\Query\Query')) + ->will($this->onConsecutiveCalls( + $this->returnCallback(function ($domain, $query) { + return Promise\reject(new TimeoutException("timeout")); + }), + $this->returnCallback(function ($domain, $query) use ($response) { + return Promise\resolve($response); + }) + )); + + $callback = $this->createCallableMock(); + $callback + ->expects($this->once()) + ->method('__invoke') + ->with($this->isInstanceOf('React\Dns\Model\Message')); + + $errorback = $this->expectCallableNever(); + + $retryExecutor = new RetryExecutor($executor, 2); + + $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); + $retryExecutor->query('8.8.8.8', $query)->then($callback, $errorback); + } + + /** + * @covers React\Dns\Query\RetryExecutor + * @test + */ + public function queryShouldStopRetryingAfterSomeAttempts() + { + $executor = $this->createExecutorMock(); + $executor + ->expects($this->exactly(3)) + ->method('query') + ->with('8.8.8.8', $this->isInstanceOf('React\Dns\Query\Query')) + ->will($this->returnCallback(function ($domain, $query) { + return Promise\reject(new TimeoutException("timeout")); + })); + + $callback = $this->expectCallableNever(); + + $errorback = $this->createCallableMock(); + $errorback + ->expects($this->once()) + ->method('__invoke') + ->with($this->isInstanceOf('RuntimeException')); + + $retryExecutor = new RetryExecutor($executor, 2); + + $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); + $retryExecutor->query('8.8.8.8', $query)->then($callback, $errorback); + } + + /** + * @covers React\Dns\Query\RetryExecutor + * @test + */ + public function queryShouldForwardNonTimeoutErrors() + { + $executor = $this->createExecutorMock(); + $executor + ->expects($this->once()) + ->method('query') + ->with('8.8.8.8', $this->isInstanceOf('React\Dns\Query\Query')) + ->will($this->returnCallback(function ($domain, $query) { + return Promise\reject(new \Exception); + })); + + $callback = $this->expectCallableNever(); + + $errorback = $this->createCallableMock(); + $errorback + ->expects($this->once()) + ->method('__invoke') + ->with($this->isInstanceOf('Exception')); + + $retryExecutor = new RetryExecutor($executor, 2); + + $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); + $retryExecutor->query('8.8.8.8', $query)->then($callback, $errorback); + } + + /** + * @covers React\Dns\Query\RetryExecutor + * @test + */ + public function queryShouldCancelQueryOnCancel() + { + $cancelled = 0; + + $executor = $this->createExecutorMock(); + $executor + ->expects($this->once()) + ->method('query') + ->with('8.8.8.8', $this->isInstanceOf('React\Dns\Query\Query')) + ->will($this->returnCallback(function ($domain, $query) use (&$cancelled) { + $deferred = new Deferred(function ($resolve, $reject) use (&$cancelled) { + ++$cancelled; + $reject(new CancellationException('Cancelled')); + }); + + return $deferred->promise(); + }) + ); + + $retryExecutor = new RetryExecutor($executor, 2); + + $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); + $promise = $retryExecutor->query('8.8.8.8', $query); + + $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); + + $this->assertEquals(0, $cancelled); + $promise->cancel(); + $this->assertEquals(1, $cancelled); + } + + protected function expectPromiseOnce($return = null) + { + $mock = $this->createPromiseMock(); + $mock + ->expects($this->once()) + ->method('then') + ->will($this->returnValue($return)); + + return $mock; + } + + protected function createExecutorMock() + { + return $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock(); + } + + protected function createPromiseMock() + { + return $this->getMockBuilder('React\Promise\PromiseInterface')->getMock(); + } + + protected function createStandardResponse() + { + $response = new Message(); + $response->header->set('qr', 1); + $response->questions[] = new Record('igor.io', Message::TYPE_A, Message::CLASS_IN); + $response->answers[] = new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131'); + $response->prepare(); + + return $response; + } +} + diff --git a/assets/php/vendor/react/dns/tests/Query/TimeoutExecutorTest.php b/assets/php/vendor/react/dns/tests/Query/TimeoutExecutorTest.php new file mode 100644 index 0000000..0d37fb4 --- /dev/null +++ b/assets/php/vendor/react/dns/tests/Query/TimeoutExecutorTest.php @@ -0,0 +1,115 @@ +<?php + +namespace React\Tests\Dns\Query; + +use React\Dns\Query\TimeoutExecutor; +use React\Dns\Query\Query; +use React\Dns\Model\Message; +use React\Promise\Deferred; +use React\Dns\Query\CancellationException; +use React\Tests\Dns\TestCase; +use React\EventLoop\Factory; +use React\Promise; + +class TimeoutExecutorTest extends TestCase +{ + public function setUp() + { + $this->loop = Factory::create(); + + $this->wrapped = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock(); + + $this->executor = new TimeoutExecutor($this->wrapped, 5.0, $this->loop); + } + + public function testCancellingPromiseWillCancelWrapped() + { + $cancelled = 0; + + $this->wrapped + ->expects($this->once()) + ->method('query') + ->will($this->returnCallback(function ($domain, $query) use (&$cancelled) { + $deferred = new Deferred(function ($resolve, $reject) use (&$cancelled) { + ++$cancelled; + $reject(new CancellationException('Cancelled')); + }); + + return $deferred->promise(); + })); + + $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); + $promise = $this->executor->query('8.8.8.8:53', $query); + + $this->assertEquals(0, $cancelled); + $promise->cancel(); + $this->assertEquals(1, $cancelled); + + $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); + } + + public function testResolvesPromiseWhenWrappedResolves() + { + $this->wrapped + ->expects($this->once()) + ->method('query') + ->willReturn(Promise\resolve('0.0.0.0')); + + $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); + $promise = $this->executor->query('8.8.8.8:53', $query); + + $promise->then($this->expectCallableOnce(), $this->expectCallableNever()); + } + + public function testRejectsPromiseWhenWrappedRejects() + { + $this->wrapped + ->expects($this->once()) + ->method('query') + ->willReturn(Promise\reject(new \RuntimeException())); + + $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); + $promise = $this->executor->query('8.8.8.8:53', $query); + + $promise->then($this->expectCallableNever(), $this->expectCallableOnceWith(new \RuntimeException())); + } + + public function testWrappedWillBeCancelledOnTimeout() + { + $this->executor = new TimeoutExecutor($this->wrapped, 0, $this->loop); + + $cancelled = 0; + + $this->wrapped + ->expects($this->once()) + ->method('query') + ->will($this->returnCallback(function ($domain, $query) use (&$cancelled) { + $deferred = new Deferred(function ($resolve, $reject) use (&$cancelled) { + ++$cancelled; + $reject(new CancellationException('Cancelled')); + }); + + return $deferred->promise(); + })); + + $callback = $this->expectCallableNever(); + + $errorback = $this->createCallableMock(); + $errorback + ->expects($this->once()) + ->method('__invoke') + ->with($this->logicalAnd( + $this->isInstanceOf('React\Dns\Query\TimeoutException'), + $this->attribute($this->equalTo('DNS query for igor.io timed out'), 'message') + )); + + $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); + $this->executor->query('8.8.8.8:53', $query)->then($callback, $errorback); + + $this->assertEquals(0, $cancelled); + + $this->loop->run(); + + $this->assertEquals(1, $cancelled); + } +} diff --git a/assets/php/vendor/react/dns/tests/Resolver/FactoryTest.php b/assets/php/vendor/react/dns/tests/Resolver/FactoryTest.php new file mode 100644 index 0000000..acaeac0 --- /dev/null +++ b/assets/php/vendor/react/dns/tests/Resolver/FactoryTest.php @@ -0,0 +1,131 @@ +<?php + +namespace React\Tests\Dns\Resolver; + +use React\Dns\Resolver\Factory; +use React\Tests\Dns\TestCase; +use React\Dns\Query\HostsFileExecutor; + +class FactoryTest extends TestCase +{ + /** @test */ + public function createShouldCreateResolver() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $factory = new Factory(); + $resolver = $factory->create('8.8.8.8:53', $loop); + + $this->assertInstanceOf('React\Dns\Resolver\Resolver', $resolver); + } + + /** @test */ + public function createWithoutPortShouldCreateResolverWithDefaultPort() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $factory = new Factory(); + $resolver = $factory->create('8.8.8.8', $loop); + + $this->assertInstanceOf('React\Dns\Resolver\Resolver', $resolver); + $this->assertSame('8.8.8.8:53', $this->getResolverPrivateMemberValue($resolver, 'nameserver')); + } + + /** @test */ + public function createCachedShouldCreateResolverWithCachedExecutor() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $factory = new Factory(); + $resolver = $factory->createCached('8.8.8.8:53', $loop); + + $this->assertInstanceOf('React\Dns\Resolver\Resolver', $resolver); + $executor = $this->getResolverPrivateExecutor($resolver); + $this->assertInstanceOf('React\Dns\Query\CachedExecutor', $executor); + $recordCache = $this->getCachedExecutorPrivateMemberValue($executor, 'cache'); + $recordCacheCache = $this->getRecordCachePrivateMemberValue($recordCache, 'cache'); + $this->assertInstanceOf('React\Cache\CacheInterface', $recordCacheCache); + $this->assertInstanceOf('React\Cache\ArrayCache', $recordCacheCache); + } + + /** @test */ + public function createCachedShouldCreateResolverWithCachedExecutorWithCustomCache() + { + $cache = $this->getMockBuilder('React\Cache\CacheInterface')->getMock(); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $factory = new Factory(); + $resolver = $factory->createCached('8.8.8.8:53', $loop, $cache); + + $this->assertInstanceOf('React\Dns\Resolver\Resolver', $resolver); + $executor = $this->getResolverPrivateExecutor($resolver); + $this->assertInstanceOf('React\Dns\Query\CachedExecutor', $executor); + $recordCache = $this->getCachedExecutorPrivateMemberValue($executor, 'cache'); + $recordCacheCache = $this->getRecordCachePrivateMemberValue($recordCache, 'cache'); + $this->assertInstanceOf('React\Cache\CacheInterface', $recordCacheCache); + $this->assertSame($cache, $recordCacheCache); + } + + /** + * @test + * @dataProvider factoryShouldAddDefaultPortProvider + */ + public function factoryShouldAddDefaultPort($input, $expected) + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $factory = new Factory(); + $resolver = $factory->create($input, $loop); + + $this->assertInstanceOf('React\Dns\Resolver\Resolver', $resolver); + $this->assertSame($expected, $this->getResolverPrivateMemberValue($resolver, 'nameserver')); + } + + public static function factoryShouldAddDefaultPortProvider() + { + return array( + array('8.8.8.8', '8.8.8.8:53'), + array('1.2.3.4:5', '1.2.3.4:5'), + array('localhost', 'localhost:53'), + array('localhost:1234', 'localhost:1234'), + array('::1', '[::1]:53'), + array('[::1]:53', '[::1]:53') + ); + } + + private function getResolverPrivateExecutor($resolver) + { + $executor = $this->getResolverPrivateMemberValue($resolver, 'executor'); + + // extract underlying executor that may be wrapped in multiple layers of hosts file executors + while ($executor instanceof HostsFileExecutor) { + $reflector = new \ReflectionProperty('React\Dns\Query\HostsFileExecutor', 'fallback'); + $reflector->setAccessible(true); + + $executor = $reflector->getValue($executor); + } + + return $executor; + } + + private function getResolverPrivateMemberValue($resolver, $field) + { + $reflector = new \ReflectionProperty('React\Dns\Resolver\Resolver', $field); + $reflector->setAccessible(true); + return $reflector->getValue($resolver); + } + + private function getCachedExecutorPrivateMemberValue($resolver, $field) + { + $reflector = new \ReflectionProperty('React\Dns\Query\CachedExecutor', $field); + $reflector->setAccessible(true); + return $reflector->getValue($resolver); + } + + private function getRecordCachePrivateMemberValue($resolver, $field) + { + $reflector = new \ReflectionProperty('React\Dns\Query\RecordCache', $field); + $reflector->setAccessible(true); + return $reflector->getValue($resolver); + } +} diff --git a/assets/php/vendor/react/dns/tests/Resolver/ResolveAliasesTest.php b/assets/php/vendor/react/dns/tests/Resolver/ResolveAliasesTest.php new file mode 100644 index 0000000..b5175e3 --- /dev/null +++ b/assets/php/vendor/react/dns/tests/Resolver/ResolveAliasesTest.php @@ -0,0 +1,100 @@ +<?php + +namespace React\Tests\Dns\Resolver; + +use PHPUnit\Framework\TestCase; +use React\Dns\Resolver\Resolver; +use React\Dns\Query\Query; +use React\Dns\Model\Message; +use React\Dns\Model\Record; + +class ResolveAliasesTest extends TestCase +{ + /** + * @covers React\Dns\Resolver\Resolver::resolveAliases + * @dataProvider provideAliasedAnswers + */ + public function testResolveAliases(array $expectedAnswers, array $answers, $name) + { + $executor = $this->createExecutorMock(); + $resolver = new Resolver('8.8.8.8:53', $executor); + + $answers = $resolver->resolveAliases($answers, $name); + + $this->assertEquals($expectedAnswers, $answers); + } + + public function provideAliasedAnswers() + { + return array( + array( + array('178.79.169.131'), + array( + new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131'), + ), + 'igor.io', + ), + array( + array('178.79.169.131', '178.79.169.132', '178.79.169.133'), + array( + new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131'), + new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.132'), + new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.133'), + ), + 'igor.io', + ), + array( + array('178.79.169.131'), + array( + new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131'), + new Record('foo.igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131'), + new Record('bar.igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131'), + ), + 'igor.io', + ), + array( + array(), + array( + new Record('foo.igor.io', Message::TYPE_A, Message::CLASS_IN), + new Record('bar.igor.io', Message::TYPE_A, Message::CLASS_IN), + ), + 'igor.io', + ), + array( + array('178.79.169.131'), + array( + new Record('igor.io', Message::TYPE_CNAME, Message::CLASS_IN, 3600, 'foo.igor.io'), + new Record('foo.igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131'), + ), + 'igor.io', + ), + array( + array('178.79.169.131'), + array( + new Record('igor.io', Message::TYPE_CNAME, Message::CLASS_IN, 3600, 'foo.igor.io'), + new Record('foo.igor.io', Message::TYPE_CNAME, Message::CLASS_IN, 3600, 'bar.igor.io'), + new Record('bar.igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131'), + ), + 'igor.io', + ), + array( + array('178.79.169.131', '178.79.169.132', '178.79.169.133'), + array( + new Record('igor.io', Message::TYPE_CNAME, Message::CLASS_IN, 3600, 'foo.igor.io'), + new Record('foo.igor.io', Message::TYPE_CNAME, Message::CLASS_IN, 3600, 'bar.igor.io'), + new Record('bar.igor.io', Message::TYPE_CNAME, Message::CLASS_IN, 3600, 'baz.igor.io'), + new Record('bar.igor.io', Message::TYPE_CNAME, Message::CLASS_IN, 3600, 'qux.igor.io'), + new Record('baz.igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131'), + new Record('baz.igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.132'), + new Record('qux.igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.133'), + ), + 'igor.io', + ), + ); + } + + private function createExecutorMock() + { + return $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock(); + } +} diff --git a/assets/php/vendor/react/dns/tests/Resolver/ResolverTest.php b/assets/php/vendor/react/dns/tests/Resolver/ResolverTest.php new file mode 100644 index 0000000..e11509b --- /dev/null +++ b/assets/php/vendor/react/dns/tests/Resolver/ResolverTest.php @@ -0,0 +1,129 @@ +<?php + +namespace React\Tests\Dns\Resolver; + +use React\Dns\Resolver\Resolver; +use React\Dns\Query\Query; +use React\Dns\Model\Message; +use React\Dns\Model\Record; +use React\Promise; +use React\Tests\Dns\TestCase; + +class ResolverTest extends TestCase +{ + /** @test */ + public function resolveShouldQueryARecords() + { + $executor = $this->createExecutorMock(); + $executor + ->expects($this->once()) + ->method('query') + ->with($this->anything(), $this->isInstanceOf('React\Dns\Query\Query')) + ->will($this->returnCallback(function ($nameserver, $query) { + $response = new Message(); + $response->header->set('qr', 1); + $response->questions[] = new Record($query->name, $query->type, $query->class); + $response->answers[] = new Record($query->name, $query->type, $query->class, 3600, '178.79.169.131'); + + return Promise\resolve($response); + })); + + $resolver = new Resolver('8.8.8.8:53', $executor); + $resolver->resolve('igor.io')->then($this->expectCallableOnceWith('178.79.169.131')); + } + + /** @test */ + public function resolveShouldQueryARecordsAndIgnoreCase() + { + $executor = $this->createExecutorMock(); + $executor + ->expects($this->once()) + ->method('query') + ->with($this->anything(), $this->isInstanceOf('React\Dns\Query\Query')) + ->will($this->returnCallback(function ($nameserver, $query) { + $response = new Message(); + $response->header->set('qr', 1); + $response->questions[] = new Record('Blog.wyrihaximus.net', $query->type, $query->class); + $response->answers[] = new Record('Blog.wyrihaximus.net', $query->type, $query->class, 3600, '178.79.169.131'); + + return Promise\resolve($response); + })); + + $resolver = new Resolver('8.8.8.8:53', $executor); + $resolver->resolve('blog.wyrihaximus.net')->then($this->expectCallableOnceWith('178.79.169.131')); + } + + /** @test */ + public function resolveShouldFilterByName() + { + $executor = $this->createExecutorMock(); + $executor + ->expects($this->once()) + ->method('query') + ->with($this->anything(), $this->isInstanceOf('React\Dns\Query\Query')) + ->will($this->returnCallback(function ($nameserver, $query) { + $response = new Message(); + $response->header->set('qr', 1); + $response->questions[] = new Record($query->name, $query->type, $query->class); + $response->answers[] = new Record('foo.bar', $query->type, $query->class, 3600, '178.79.169.131'); + + return Promise\resolve($response); + })); + + $errback = $this->expectCallableOnceWith($this->isInstanceOf('React\Dns\RecordNotFoundException')); + + $resolver = new Resolver('8.8.8.8:53', $executor); + $resolver->resolve('igor.io')->then($this->expectCallableNever(), $errback); + } + + /** @test */ + public function resolveWithNoAnswersShouldThrowException() + { + $executor = $this->createExecutorMock(); + $executor + ->expects($this->once()) + ->method('query') + ->with($this->anything(), $this->isInstanceOf('React\Dns\Query\Query')) + ->will($this->returnCallback(function ($nameserver, $query) { + $response = new Message(); + $response->header->set('qr', 1); + $response->questions[] = new Record($query->name, $query->type, $query->class); + + return Promise\resolve($response); + })); + + $errback = $this->expectCallableOnceWith($this->isInstanceOf('React\Dns\RecordNotFoundException')); + + $resolver = new Resolver('8.8.8.8:53', $executor); + $resolver->resolve('igor.io')->then($this->expectCallableNever(), $errback); + } + + /** + * @test + */ + public function resolveWithNoAnswersShouldCallErrbackIfGiven() + { + $executor = $this->createExecutorMock(); + $executor + ->expects($this->once()) + ->method('query') + ->with($this->anything(), $this->isInstanceOf('React\Dns\Query\Query')) + ->will($this->returnCallback(function ($nameserver, $query) { + $response = new Message(); + $response->header->set('qr', 1); + $response->questions[] = new Record($query->name, $query->type, $query->class); + + return Promise\resolve($response); + })); + + $errback = $this->expectCallableOnceWith($this->isInstanceOf('React\Dns\RecordNotFoundException')); + + $resolver = new Resolver('8.8.8.8:53', $executor); + $resolver->resolve('igor.io')->then($this->expectCallableNever(), $errback); + } + + private function createExecutorMock() + { + return $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock(); + } +} diff --git a/assets/php/vendor/react/dns/tests/TestCase.php b/assets/php/vendor/react/dns/tests/TestCase.php new file mode 100644 index 0000000..a5a22bf --- /dev/null +++ b/assets/php/vendor/react/dns/tests/TestCase.php @@ -0,0 +1,61 @@ +<?php + +namespace React\Tests\Dns; + +use PHPUnit\Framework\TestCase as BaseTestCase; + +abstract class TestCase extends BaseTestCase +{ + protected function expectCallableOnce() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke'); + + return $mock; + } + + protected function expectCallableOnceWith($value) + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($value); + + return $mock; + } + + protected function expectCallableNever() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->never()) + ->method('__invoke'); + + return $mock; + } + + protected function createCallableMock() + { + return $this->getMockBuilder('React\Tests\Dns\CallableStub')->getMock(); + } + + public function setExpectedException($exception, $exceptionMessage = '', $exceptionCode = null) + { + if (method_exists($this, 'expectException')) { + // PHPUnit 5 + $this->expectException($exception); + if ($exceptionMessage !== '') { + $this->expectExceptionMessage($exceptionMessage); + } + if ($exceptionCode !== null) { + $this->expectExceptionCode($exceptionCode); + } + } else { + // legacy PHPUnit 4 + parent::setExpectedException($exception, $exceptionMessage, $exceptionCode); + } + } +} diff --git a/assets/php/vendor/react/event-loop/.gitignore b/assets/php/vendor/react/event-loop/.gitignore new file mode 100644 index 0000000..81b9258 --- /dev/null +++ b/assets/php/vendor/react/event-loop/.gitignore @@ -0,0 +1,3 @@ +composer.lock +phpunit.xml +vendor diff --git a/assets/php/vendor/react/event-loop/.travis.yml b/assets/php/vendor/react/event-loop/.travis.yml new file mode 100644 index 0000000..7af713a --- /dev/null +++ b/assets/php/vendor/react/event-loop/.travis.yml @@ -0,0 +1,39 @@ +language: php + +php: +# - 5.3 # requires old distro, see below + - 5.4 + - 5.5 + - 5.6 + - 7.0 + - 7.1 + - 7.2 + - hhvm # ignore errors, see below + +# lock distro so new future defaults will not break the build +dist: trusty + +matrix: + include: + - php: 5.3 + dist: precise + allow_failures: + - php: hhvm + +sudo: false + +addons: + apt: + packages: + - libevent-dev # Used by 'event' and 'libevent' PHP extensions + +cache: + directories: + - $HOME/.composer/cache/files + +install: + - ./travis-init.sh + - composer install + +script: + - ./vendor/bin/phpunit --coverage-text diff --git a/assets/php/vendor/react/event-loop/CHANGELOG.md b/assets/php/vendor/react/event-loop/CHANGELOG.md new file mode 100644 index 0000000..c291840 --- /dev/null +++ b/assets/php/vendor/react/event-loop/CHANGELOG.md @@ -0,0 +1,316 @@ +# Changelog + +## 0.5.1 (2018-04-09) + +* New loop: ExtEvLoop (PECL ext-ev) (#148 by @kaduev13) + +## 0.5.0 (2018-04-05) + +A major feature release with a significant documentation overhaul and long overdue API cleanup! + +This update involves a number of BC breaks due to dropped support for deprecated +functionality. We've tried hard to avoid BC breaks where possible and minimize +impact otherwise. We expect that most consumers of this package will actually +not be affected by any BC breaks, see below for more details. + +We realize that the changes listed below may seem overwhelming, but we've tried +to be very clear about any possible BC breaks. Don't worry: In fact, all ReactPHP +components are already compatible and support both this new release as well as +providing backwards compatibility with the last release. + +* Feature / BC break: Add support for signal handling via new + `LoopInterface::addSignal()` and `LoopInterface::removeSignal()` methods. + (#104 by @WyriHaximus and #111 and #150 by @clue) + + ```php + $loop->addSignal(SIGINT, function () { + echo 'CTRL-C'; + }); + ``` + +* Feature: Significant documentation updates for `LoopInterface` and `Factory`. + (#100, #119, #126, #127, #159 and #160 by @clue, #113 by @WyriHaximus and #81 and #91 by @jsor) + +* Feature: Add examples to ease getting started + (#99, #100 and #125 by @clue, #59 by @WyriHaximus and #143 by @jsor) + +* Feature: Documentation for advanced timer concepts, such as monotonic time source vs wall-clock time + and high precision timers with millisecond accuracy or below. + (#130 and #157 by @clue) + +* Feature: Documentation for advanced stream concepts, such as edge-triggered event listeners + and stream buffers and allow throwing Exception if stream resource is not supported. + (#129 and #158 by @clue) + +* Feature: Throw `BadMethodCallException` on manual loop creation when required extension isn't installed. + (#153 by @WyriHaximus) + +* Feature / BC break: First class support for legacy PHP 5.3 through PHP 7.2 and HHVM + and remove all `callable` type hints for consistency reasons. + (#141 and #151 by @clue) + +* BC break: Documentation for timer API and clean up unneeded timer API. + (#102 by @clue) + + Remove `TimerInterface::cancel()`, use `LoopInterface::cancelTimer()` instead: + + ```php + // old (method invoked on timer instance) + $timer->cancel(); + + // already supported before: invoke method on loop instance + $loop->cancelTimer($timer); + ``` + + Remove unneeded `TimerInterface::setData()` and `TimerInterface::getData()`, + use closure binding to add arbitrary data to timer instead: + + ```php + // old (limited setData() and getData() only allows single variable) + $name = 'Tester'; + $timer = $loop->addTimer(1.0, function ($timer) { + echo 'Hello ' . $timer->getData() . PHP_EOL; + }); + $timer->setData($name); + + // already supported before: closure binding allows any number of variables + $name = 'Tester'; + $loop->addTimer(1.0, function () use ($name) { + echo 'Hello ' . $name . PHP_EOL; + }); + ``` + + Remove unneeded `TimerInterface::getLoop()`, use closure binding instead: + + ```php + // old (getLoop() called on timer instance) + $loop->addTimer(0.1, function ($timer) { + $timer->getLoop()->stop(); + }); + + // already supported before: use closure binding as usual + $loop->addTimer(0.1, function () use ($loop) { + $loop->stop(); + }); + ``` + +* BC break: Remove unneeded `LoopInterface::isTimerActive()` and + `TimerInterface::isActive()` to reduce API surface. + (#133 by @clue) + + ```php + // old (method on timer instance or on loop instance) + $timer->isActive(); + $loop->isTimerActive($timer); + ``` + +* BC break: Move `TimerInterface` one level up to `React\EventLoop\TimerInterface`. + (#138 by @WyriHaximus) + + ```php + // old (notice obsolete "Timer" namespace) + assert($timer instanceof React\EventLoop\Timer\TimerInterface); + + // new + assert($timer instanceof React\EventLoop\TimerInterface); + ``` + +* BC break: Remove unneeded `LoopInterface::nextTick()` (and internal `NextTickQueue`), + use `LoopInterface::futureTick()` instead. + (#30 by @clue) + + ```php + // old (removed) + $loop->nextTick(function () { + echo 'tick'; + }); + + // already supported before + $loop->futureTick(function () { + echo 'tick'; + }); + ``` + +* BC break: Remove unneeded `$loop` argument for `LoopInterface::futureTick()` + (and fix internal cyclic dependency). + (#103 by @clue) + + ```php + // old ($loop gets passed by default) + $loop->futureTick(function ($loop) { + $loop->stop(); + }); + + // already supported before: use closure binding as usual + $loop->futureTick(function () use ($loop) { + $loop->stop(); + }); + ``` + +* BC break: Remove unneeded `LoopInterface::tick()`. + (#72 by @jsor) + + ```php + // old (removed) + $loop->tick(); + + // suggested work around for testing purposes only + $loop->futureTick(function () use ($loop) { + $loop->stop(); + }); + ``` + +* BC break: Documentation for advanced stream API and clean up unneeded stream API. + (#110 by @clue) + + Remove unneeded `$loop` argument for `LoopInterface::addReadStream()` + and `LoopInterface::addWriteStream()`, use closure binding instead: + + ```php + // old ($loop gets passed by default) + $loop->addReadStream($stream, function ($stream, $loop) { + $loop->removeReadStream($stream); + }); + + // already supported before: use closure binding as usual + $loop->addReadStream($stream, function ($stream) use ($loop) { + $loop->removeReadStream($stream); + }); + ``` + +* BC break: Remove unneeded `LoopInterface::removeStream()` method, + use `LoopInterface::removeReadStream()` and `LoopInterface::removeWriteStream()` instead. + (#118 by @clue) + + ```php + // old + $loop->removeStream($stream); + + // already supported before + $loop->removeReadStream($stream); + $loop->removeWriteStream($stream); + ``` + +* BC break: Rename `LibEventLoop` to `ExtLibeventLoop` and `LibEvLoop` to `ExtLibevLoop` + for consistent naming for event loop implementations. + (#128 by @clue) + +* BC break: Remove optional `EventBaseConfig` argument from `ExtEventLoop` + and make its `FEATURE_FDS` enabled by default. + (#156 by @WyriHaximus) + +* BC break: Mark all classes as final to discourage inheritance. + (#131 by @clue) + +* Fix: Fix `ExtEventLoop` to keep track of stream resources (refcount) + (#123 by @clue) + +* Fix: Ensure large timer interval does not overflow on 32bit systems + (#132 by @clue) + +* Fix: Fix separately removing readable and writable side of stream when closing + (#139 by @clue) + +* Fix: Properly clean up event watchers for `ext-event` and `ext-libev` + (#149 by @clue) + +* Fix: Minor code cleanup and remove unneeded references + (#145 by @seregazhuk) + +* Fix: Discourage outdated `ext-libevent` on PHP 7 + (#62 by @cboden) + +* Improve test suite by adding forward compatibility with PHPUnit 6 and PHPUnit 5, + lock Travis distro so new defaults will not break the build, + improve test suite to be less fragile and increase test timeouts, + test against PHP 7.2 and reduce fwrite() call length to one chunk. + (#106 and #144 by @clue, #120 and #124 by @carusogabriel, #147 by nawarian and #92 by @kelunik) + +* A number of changes were originally planned for this release but have been backported + to the last `v0.4.3` already: #74, #76, #79, #81 (refs #65, #66, #67), #88 and #93 + +## 0.4.3 (2017-04-27) + +* Bug fix: Bugfix in the usage sample code #57 (@dandelionred) +* Improvement: Remove branch-alias definition #53 (@WyriHaximus) +* Improvement: StreamSelectLoop: Use fresh time so Timers added during stream events are accurate #51 (@andrewminerd) +* Improvement: Avoid deprecation warnings in test suite due to deprecation of getMock() in PHPUnit #68 (@martinschroeder) +* Improvement: Add PHPUnit 4.8 to require-dev #69 (@shaunbramley) +* Improvement: Increase test timeouts for HHVM and unify timeout handling #70 (@clue) +* Improvement: Travis improvements (backported from #74) #75 (@clue) +* Improvement: Test suite now uses socket pairs instead of memory streams #66 (@martinschroeder) +* Improvement: StreamSelectLoop: Test suite uses signal constant names in data provider #67 (@martinschroeder) +* Improvement: ExtEventLoop: No longer suppress all errors #65 (@mamciek) +* Improvement: Readme cleanup #89 (@jsor) +* Improvement: Restructure and improve README #90 (@jsor) +* Bug fix: StreamSelectLoop: Fix erroneous zero-time sleep (backport to 0.4) #94 (@jsor) + +## 0.4.2 (2016-03-07) + +* Bug fix: No longer error when signals sent to StreamSelectLoop +* Support HHVM and PHP7 (@ondrejmirtes, @cebe) +* Feature: Added support for EventConfig for ExtEventLoop (@steverhoades) +* Bug fix: Fixed an issue loading loop extension libs via autoloader (@czarpino) + +## 0.4.1 (2014-04-13) + +* Bug fix: null timeout in StreamSelectLoop causing 100% CPU usage (@clue) +* Bug fix: v0.3.4 changes merged for v0.4.1 + +## 0.4.0 (2014-02-02) + +* Feature: Added `EventLoopInterface::nextTick()`, implemented in all event loops (@jmalloc) +* Feature: Added `EventLoopInterface::futureTick()`, implemented in all event loops (@jmalloc) +* Feature: Added `ExtEventLoop` implementation using pecl/event (@jmalloc) +* BC break: Bump minimum PHP version to PHP 5.4, remove 5.3 specific hacks +* BC break: New method: `EventLoopInterface::nextTick()` +* BC break: New method: `EventLoopInterface::futureTick()` +* Dependency: Autoloading and filesystem structure now PSR-4 instead of PSR-0 + +## 0.3.5 (2016-12-28) + +This is a compatibility release that eases upgrading to the v0.4 release branch. +You should consider upgrading to the v0.4 release branch. + +* Feature: Cap min timer interval at 1µs, thus improving compatibility with v0.4 + (#47 by @clue) + +## 0.3.4 (2014-03-30) + +* Bug fix: Changed StreamSelectLoop to use non-blocking behavior on tick() (@astephens25) + +## 0.3.3 (2013-07-08) + +* Bug fix: No error on removing non-existent streams (@clue) +* Bug fix: Do not silently remove feof listeners in `LibEvLoop` + +## 0.3.0 (2013-04-14) + +* BC break: New timers API (@nrk) +* BC break: Remove check on return value from stream callbacks (@nrk) + +## 0.2.7 (2013-01-05) + +* Bug fix: Fix libevent timers with PHP 5.3 +* Bug fix: Fix libevent timer cancellation (@nrk) + +## 0.2.6 (2012-12-26) + +* Bug fix: Plug memory issue in libevent timers (@cameronjacobson) +* Bug fix: Correctly pause LibEvLoop on stop() + +## 0.2.3 (2012-11-14) + +* Feature: LibEvLoop, integration of `php-libev` + +## 0.2.0 (2012-09-10) + +* Version bump + +## 0.1.1 (2012-07-12) + +* Version bump + +## 0.1.0 (2012-07-11) + +* First tagged release diff --git a/assets/php/vendor/react/event-loop/LICENSE b/assets/php/vendor/react/event-loop/LICENSE new file mode 100644 index 0000000..a808108 --- /dev/null +++ b/assets/php/vendor/react/event-loop/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2012 Igor Wiedler, Chris Boden + +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/assets/php/vendor/react/event-loop/README.md b/assets/php/vendor/react/event-loop/README.md new file mode 100644 index 0000000..207e7f4 --- /dev/null +++ b/assets/php/vendor/react/event-loop/README.md @@ -0,0 +1,702 @@ +# EventLoop Component + +[](https://travis-ci.org/reactphp/event-loop) + +[ReactPHP](https://reactphp.org/)'s core reactor event loop that libraries can use for evented I/O. + +In order for async based libraries to be interoperable, they need to use the +same event loop. This component provides a common `LoopInterface` that any +library can target. This allows them to be used in the same loop, with one +single [`run()`](#run) call that is controlled by the user. + +**Table of Contents** + +* [Quickstart example](#quickstart-example) +* [Usage](#usage) + * [Factory](#factory) + * [create()](#create) + * [Loop implementations](#loop-implementations) + * [StreamSelectLoop](#streamselectloop) + * [ExtEventLoop](#exteventloop) + * [ExtLibeventLoop](#extlibeventloop) + * [ExtLibevLoop](#extlibevloop) + * [ExtEvLoop](#extevloop) + * [LoopInterface](#loopinterface) + * [run()](#run) + * [stop()](#stop) + * [addTimer()](#addtimer) + * [addPeriodicTimer()](#addperiodictimer) + * [cancelTimer()](#canceltimer) + * [futureTick()](#futuretick) + * [addSignal()](#addsignal) + * [removeSignal()](#removesignal) + * [addReadStream()](#addreadstream) + * [addWriteStream()](#addwritestream) + * [removeReadStream()](#removereadstream) + * [removeWriteStream()](#removewritestream) +* [Install](#install) +* [Tests](#tests) +* [License](#license) +* [More](#more) + +## Quickstart example + +Here is an async HTTP server built with just the event loop. + +```php +$loop = React\EventLoop\Factory::create(); + +$server = stream_socket_server('tcp://127.0.0.1:8080'); +stream_set_blocking($server, false); + +$loop->addReadStream($server, function ($server) use ($loop) { + $conn = stream_socket_accept($server); + $data = "HTTP/1.1 200 OK\r\nContent-Length: 3\r\n\r\nHi\n"; + $loop->addWriteStream($conn, function ($conn) use (&$data, $loop) { + $written = fwrite($conn, $data); + if ($written === strlen($data)) { + fclose($conn); + $loop->removeWriteStream($conn); + } else { + $data = substr($data, $written); + } + }); +}); + +$loop->addPeriodicTimer(5, function () { + $memory = memory_get_usage() / 1024; + $formatted = number_format($memory, 3).'K'; + echo "Current memory usage: {$formatted}\n"; +}); + +$loop->run(); +``` + +See also the [examples](examples). + +## Usage + +Typical applications use a single event loop which is created at the beginning +and run at the end of the program. + +```php +// [1] +$loop = React\EventLoop\Factory::create(); + +// [2] +$loop->addPeriodicTimer(1, function () { + echo "Tick\n"; +}); + +$stream = new React\Stream\ReadableResourceStream( + fopen('file.txt', 'r'), + $loop +); + +// [3] +$loop->run(); +``` + +1. The loop instance is created at the beginning of the program. A convenience + factory [`React\EventLoop\Factory::create()`](#create) is provided by this library which + picks the best available [loop implementation](#loop-implementations). +2. The loop instance is used directly or passed to library and application code. + In this example, a periodic timer is registered with the event loop which + simply outputs `Tick` every second and a + [readable stream](https://github.com/reactphp/stream#readableresourcestream) + is created by using ReactPHP's + [stream component](https://github.com/reactphp/stream) for demonstration + purposes. +3. The loop is run with a single [`$loop->run()`](#run) call at the end of the program. + +### Factory + +The `Factory` class exists as a convenient way to pick the best available +[event loop implementation](#loop-implementations). + +#### create() + +The `create(): LoopInterface` method can be used to create a new event loop +instance: + +```php +$loop = React\EventLoop\Factory::create(); +``` + +This method always returns an instance implementing [`LoopInterface`](#loopinterface), +the actual [event loop implementation](#loop-implementations) is an implementation detail. + +This method should usually only be called once at the beginning of the program. + +### Loop implementations + +In addition to the [`LoopInterface`](#loopinterface), there are a number of +event loop implementations provided. + +All of the event loops support these features: + +* File descriptor polling +* One-off timers +* Periodic timers +* Deferred execution on future loop tick + +For most consumers of this package, the underlying event loop implementation is +an implementation detail. +You should use the [`Factory`](#factory) to automatically create a new instance. + +Advanced! If you explicitly need a certain event loop implementation, you can +manually instantiate one of the following classes. +Note that you may have to install the required PHP extensions for the respective +event loop implementation first or they will throw a `BadMethodCallException` on creation. + +#### StreamSelectLoop + +A `stream_select()` based event loop. + +This uses the [`stream_select()`](http://php.net/manual/en/function.stream-select.php) +function and is the only implementation which works out of the box with PHP. + +This event loop works out of the box on PHP 5.3 through PHP 7+ and HHVM. +This means that no installation is required and this library works on all +platforms and supported PHP versions. +Accordingly, the [`Factory`](#factory) will use this event loop by default if +you do not install any of the event loop extensions listed below. + +Under the hood, it does a simple `select` system call. +This system call is limited to the maximum file descriptor number of +`FD_SETSIZE` (platform dependent, commonly 1024) and scales with `O(m)` +(`m` being the maximum file descriptor number passed). +This means that you may run into issues when handling thousands of streams +concurrently and you may want to look into using one of the alternative +event loop implementations listed below in this case. +If your use case is among the many common use cases that involve handling only +dozens or a few hundred streams at once, then this event loop implementation +performs really well. + +If you want to use signal handling (see also [`addSignal()`](#addsignal) below), +this event loop implementation requires `ext-pcntl`. +This extension is only available for Unix-like platforms and does not support +Windows. +It is commonly installed as part of many PHP distributions. +If this extension is missing (or you're running on Windows), signal handling is +not supported and throws a `BadMethodCallException` instead. + +This event loop is known to rely on wall-clock time to schedule future +timers, because a monotonic time source is not available in PHP by default. +While this does not affect many common use cases, this is an important +distinction for programs that rely on a high time precision or on systems +that are subject to discontinuous time adjustments (time jumps). +This means that if you schedule a timer to trigger in 30s and then adjust +your system time forward by 20s, the timer may trigger in 10s. +See also [`addTimer()`](#addtimer) for more details. + +#### ExtEventLoop + +An `ext-event` based event loop. + +This uses the [`event` PECL extension](https://pecl.php.net/package/event). +It supports the same backends as libevent. + +This loop is known to work with PHP 5.4 through PHP 7+. + +#### ExtEvLoop + +An `ext-ev` based event loop. + +This loop uses the [`ev` PECL extension](https://pecl.php.net/package/ev), that +provides an interface to `libev` library. + +This loop is known to work with PHP 5.4 through PHP 7+. + + +#### ExtLibeventLoop + +An `ext-libevent` based event loop. + +This uses the [`libevent` PECL extension](https://pecl.php.net/package/libevent). +`libevent` itself supports a number of system-specific backends (epoll, kqueue). + +This event loop does only work with PHP 5. +An [unofficial update](https://github.com/php/pecl-event-libevent/pull/2) for +PHP 7 does exist, but it is known to cause regular crashes due to `SEGFAULT`s. +To reiterate: Using this event loop on PHP 7 is not recommended. +Accordingly, the [`Factory`](#factory) will not try to use this event loop on +PHP 7. + +This event loop is known to trigger a readable listener only if +the stream *becomes* readable (edge-triggered) and may not trigger if the +stream has already been readable from the beginning. +This also implies that a stream may not be recognized as readable when data +is still left in PHP's internal stream buffers. +As such, it's recommended to use `stream_set_read_buffer($stream, 0);` +to disable PHP's internal read buffer in this case. +See also [`addReadStream()`](#addreadstream) for more details. + +#### ExtLibevLoop + +An `ext-libev` based event loop. + +This uses an [unofficial `libev` extension](https://github.com/m4rw3r/php-libev). +It supports the same backends as libevent. + +This loop does only work with PHP 5. +An update for PHP 7 is [unlikely](https://github.com/m4rw3r/php-libev/issues/8) +to happen any time soon. + +### LoopInterface + +#### run() + +The `run(): void` method can be used to +run the event loop until there are no more tasks to perform. + +For many applications, this method is the only directly visible +invocation on the event loop. +As a rule of thumb, it is usally recommended to attach everything to the +same loop instance and then run the loop once at the bottom end of the +application. + +```php +$loop->run(); +``` + +This method will keep the loop running until there are no more tasks +to perform. In other words: This method will block until the last +timer, stream and/or signal has been removed. + +Likewise, it is imperative to ensure the application actually invokes +this method once. Adding listeners to the loop and missing to actually +run it will result in the application exiting without actually waiting +for any of the attached listeners. + +This method MUST NOT be called while the loop is already running. +This method MAY be called more than once after it has explicity been +[`stop()`ped](#stop) or after it automatically stopped because it +previously did no longer have anything to do. + +#### stop() + +The `stop(): void` method can be used to +instruct a running event loop to stop. + +This method is considered advanced usage and should be used with care. +As a rule of thumb, it is usually recommended to let the loop stop +only automatically when it no longer has anything to do. + +This method can be used to explicitly instruct the event loop to stop: + +```php +$loop->addTimer(3.0, function () use ($loop) { + $loop->stop(); +}); +``` + +Calling this method on a loop instance that is not currently running or +on a loop instance that has already been stopped has no effect. + +#### addTimer() + +The `addTimer(float $interval, callable $callback): TimerInterface` method can be used to +enqueue a callback to be invoked once after the given interval. + +The timer callback function MUST be able to accept a single parameter, +the timer instance as also returned by this method or you MAY use a +function which has no parameters at all. + +The timer callback function MUST NOT throw an `Exception`. +The return value of the timer callback function will be ignored and has +no effect, so for performance reasons you're recommended to not return +any excessive data structures. + +Unlike [`addPeriodicTimer()`](#addperiodictimer), this method will ensure +the callback will be invoked only once after the given interval. +You can invoke [`cancelTimer`](#canceltimer) to cancel a pending timer. + +```php +$loop->addTimer(0.8, function () { + echo 'world!' . PHP_EOL; +}); + +$loop->addTimer(0.3, function () { + echo 'hello '; +}); +``` + +See also [example #1](examples). + +If you want to access any variables within your callback function, you +can bind arbitrary data to a callback closure like this: + +```php +function hello($name, LoopInterface $loop) +{ + $loop->addTimer(1.0, function () use ($name) { + echo "hello $name\n"; + }); +} + +hello('Tester', $loop); +``` + +This interface does not enforce any particular timer resolution, so +special care may have to be taken if you rely on very high precision with +millisecond accuracy or below. Event loop implementations SHOULD work on +a best effort basis and SHOULD provide at least millisecond accuracy +unless otherwise noted. Many existing event loop implementations are +known to provide microsecond accuracy, but it's generally not recommended +to rely on this high precision. + +Similarly, the execution order of timers scheduled to execute at the +same time (within its possible accuracy) is not guaranteed. + +This interface suggests that event loop implementations SHOULD use a +monotonic time source if available. Given that a monotonic time source is +not available on PHP by default, event loop implementations MAY fall back +to using wall-clock time. +While this does not affect many common use cases, this is an important +distinction for programs that rely on a high time precision or on systems +that are subject to discontinuous time adjustments (time jumps). +This means that if you schedule a timer to trigger in 30s and then adjust +your system time forward by 20s, the timer SHOULD still trigger in 30s. +See also [event loop implementations](#loop-implementations) for more details. + +#### addPeriodicTimer() + +The `addPeriodicTimer(float $interval, callable $callback): TimerInterface` method can be used to +enqueue a callback to be invoked repeatedly after the given interval. + +The timer callback function MUST be able to accept a single parameter, +the timer instance as also returned by this method or you MAY use a +function which has no parameters at all. + +The timer callback function MUST NOT throw an `Exception`. +The return value of the timer callback function will be ignored and has +no effect, so for performance reasons you're recommended to not return +any excessive data structures. + +Unlike [`addTimer()`](#addtimer), this method will ensure the the +callback will be invoked infinitely after the given interval or until you +invoke [`cancelTimer`](#canceltimer). + +```php +$timer = $loop->addPeriodicTimer(0.1, function () { + echo 'tick!' . PHP_EOL; +}); + +$loop->addTimer(1.0, function () use ($loop, $timer) { + $loop->cancelTimer($timer); + echo 'Done' . PHP_EOL; +}); +``` + +See also [example #2](examples). + +If you want to limit the number of executions, you can bind +arbitrary data to a callback closure like this: + +```php +function hello($name, LoopInterface $loop) +{ + $n = 3; + $loop->addPeriodicTimer(1.0, function ($timer) use ($name, $loop, &$n) { + if ($n > 0) { + --$n; + echo "hello $name\n"; + } else { + $loop->cancelTimer($timer); + } + }); +} + +hello('Tester', $loop); +``` + +This interface does not enforce any particular timer resolution, so +special care may have to be taken if you rely on very high precision with +millisecond accuracy or below. Event loop implementations SHOULD work on +a best effort basis and SHOULD provide at least millisecond accuracy +unless otherwise noted. Many existing event loop implementations are +known to provide microsecond accuracy, but it's generally not recommended +to rely on this high precision. + +Similarly, the execution order of timers scheduled to execute at the +same time (within its possible accuracy) is not guaranteed. + +This interface suggests that event loop implementations SHOULD use a +monotonic time source if available. Given that a monotonic time source is +not available on PHP by default, event loop implementations MAY fall back +to using wall-clock time. +While this does not affect many common use cases, this is an important +distinction for programs that rely on a high time precision or on systems +that are subject to discontinuous time adjustments (time jumps). +This means that if you schedule a timer to trigger in 30s and then adjust +your system time forward by 20s, the timer SHOULD still trigger in 30s. +See also [event loop implementations](#loop-implementations) for more details. + +Additionally, periodic timers may be subject to timer drift due to +re-scheduling after each invocation. As such, it's generally not +recommended to rely on this for high precision intervals with millisecond +accuracy or below. + +#### cancelTimer() + +The `cancelTimer(TimerInterface $timer): void` method can be used to +cancel a pending timer. + +See also [`addPeriodicTimer()`](#addperiodictimer) and [example #2](examples). + +Calling this method on a timer instance that has not been added to this +loop instance or on a timer that has already been cancelled has no effect. + +#### futureTick() + +The `futureTick(callable $listener): void` method can be used to +schedule a callback to be invoked on a future tick of the event loop. + +This works very much similar to timers with an interval of zero seconds, +but does not require the overhead of scheduling a timer queue. + +The tick callback function MUST be able to accept zero parameters. + +The tick callback function MUST NOT throw an `Exception`. +The return value of the tick callback function will be ignored and has +no effect, so for performance reasons you're recommended to not return +any excessive data structures. + +If you want to access any variables within your callback function, you +can bind arbitrary data to a callback closure like this: + +```php +function hello($name, LoopInterface $loop) +{ + $loop->futureTick(function () use ($name) { + echo "hello $name\n"; + }); +} + +hello('Tester', $loop); +``` + +Unlike timers, tick callbacks are guaranteed to be executed in the order +they are enqueued. +Also, once a callback is enqueued, there's no way to cancel this operation. + +This is often used to break down bigger tasks into smaller steps (a form +of cooperative multitasking). + +```php +$loop->futureTick(function () { + echo 'b'; +}); +$loop->futureTick(function () { + echo 'c'; +}); +echo 'a'; +``` + +See also [example #3](examples). + +#### addSignal() + +The `addSignal(int $signal, callable $listener): void` method can be used to +register a listener to be notified when a signal has been caught by this process. + +This is useful to catch user interrupt signals or shutdown signals from +tools like `supervisor` or `systemd`. + +The listener callback function MUST be able to accept a single parameter, +the signal added by this method or you MAY use a function which +has no parameters at all. + +The listener callback function MUST NOT throw an `Exception`. +The return value of the listener callback function will be ignored and has +no effect, so for performance reasons you're recommended to not return +any excessive data structures. + +```php +$loop->addSignal(SIGINT, function (int $signal) { + echo 'Caught user interrupt signal' . PHP_EOL; +}); +``` + +See also [example #4](examples). + +Signaling is only available on Unix-like platform, Windows isn't +supported due to operating system limitations. +This method may throw a `BadMethodCallException` if signals aren't +supported on this platform, for example when required extensions are +missing. + +**Note: A listener can only be added once to the same signal, any +attempts to add it more then once will be ignored.** + +#### removeSignal() + +The `removeSignal(int $signal, callable $listener): void` method can be used to +remove a previously added signal listener. + +```php +$loop->removeSignal(SIGINT, $listener); +``` + +Any attempts to remove listeners that aren't registered will be ignored. + +#### addReadStream() + +> Advanced! Note that this low-level API is considered advanced usage. + Most use cases should probably use the higher-level + [readable Stream API](https://github.com/reactphp/stream#readablestreaminterface) + instead. + +The `addReadStream(resource $stream, callable $callback): void` method can be used to +register a listener to be notified when a stream is ready to read. + +The first parameter MUST be a valid stream resource that supports +checking whether it is ready to read by this loop implementation. +A single stream resource MUST NOT be added more than once. +Instead, either call [`removeReadStream()`](#removereadstream) first or +react to this event with a single listener and then dispatch from this +listener. This method MAY throw an `Exception` if the given resource type +is not supported by this loop implementation. + +The listener callback function MUST be able to accept a single parameter, +the stream resource added by this method or you MAY use a function which +has no parameters at all. + +The listener callback function MUST NOT throw an `Exception`. +The return value of the listener callback function will be ignored and has +no effect, so for performance reasons you're recommended to not return +any excessive data structures. + +If you want to access any variables within your callback function, you +can bind arbitrary data to a callback closure like this: + +```php +$loop->addReadStream($stream, function ($stream) use ($name) { + echo $name . ' said: ' . fread($stream); +}); +``` + +See also [example #11](examples). + +You can invoke [`removeReadStream()`](#removereadstream) to remove the +read event listener for this stream. + +The execution order of listeners when multiple streams become ready at +the same time is not guaranteed. + +Some event loop implementations are known to only trigger the listener if +the stream *becomes* readable (edge-triggered) and may not trigger if the +stream has already been readable from the beginning. +This also implies that a stream may not be recognized as readable when data +is still left in PHP's internal stream buffers. +As such, it's recommended to use `stream_set_read_buffer($stream, 0);` +to disable PHP's internal read buffer in this case. + +#### addWriteStream() + +> Advanced! Note that this low-level API is considered advanced usage. + Most use cases should probably use the higher-level + [writable Stream API](https://github.com/reactphp/stream#writablestreaminterface) + instead. + +The `addWriteStream(resource $stream, callable $callback): void` method can be used to +register a listener to be notified when a stream is ready to write. + +The first parameter MUST be a valid stream resource that supports +checking whether it is ready to write by this loop implementation. +A single stream resource MUST NOT be added more than once. +Instead, either call [`removeWriteStream()`](#removewritestream) first or +react to this event with a single listener and then dispatch from this +listener. This method MAY throw an `Exception` if the given resource type +is not supported by this loop implementation. + +The listener callback function MUST be able to accept a single parameter, +the stream resource added by this method or you MAY use a function which +has no parameters at all. + +The listener callback function MUST NOT throw an `Exception`. +The return value of the listener callback function will be ignored and has +no effect, so for performance reasons you're recommended to not return +any excessive data structures. + +If you want to access any variables within your callback function, you +can bind arbitrary data to a callback closure like this: + +```php +$loop->addWriteStream($stream, function ($stream) use ($name) { + fwrite($stream, 'Hello ' . $name); +}); +``` + +See also [example #12](examples). + +You can invoke [`removeWriteStream()`](#removewritestream) to remove the +write event listener for this stream. + +The execution order of listeners when multiple streams become ready at +the same time is not guaranteed. + +#### removeReadStream() + +The `removeReadStream(resource $stream): void` method can be used to +remove the read event listener for the given stream. + +Removing a stream from the loop that has already been removed or trying +to remove a stream that was never added or is invalid has no effect. + +#### removeWriteStream() + +The `removeWriteStream(resource $stream): void` method can be used to +remove the write event listener for the given stream. + +Removing a stream from the loop that has already been removed or trying +to remove a stream that was never added or is invalid has no effect. + +## Install + +The recommended way to install this library is [through Composer](https://getcomposer.org). +[New to Composer?](https://getcomposer.org/doc/00-intro.md) + +This will install the latest supported version: + +```bash +$ composer require react/event-loop:^0.5.1 +``` + +See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. + +This project aims to run on any platform and thus does not require any PHP +extensions and supports running on legacy PHP 5.3 through current PHP 7+ and +HHVM. +It's *highly recommended to use PHP 7+* for this project. + +Installing any of the event loop extensions is suggested, but entirely optional. +See also [event loop implementations](#loop-implementations) for more details. + +## Tests + +To run the test suite, you first need to clone this repo and then install all +dependencies [through Composer](https://getcomposer.org): + +```bash +$ composer install +``` + +To run the test suite, go to the project root and run: + +```bash +$ php vendor/bin/phpunit +``` + +## License + +MIT, see [LICENSE file](LICENSE). + +## More + +* See our [Stream component](https://github.com/reactphp/stream) for more + information on how streams are used in real-world applications. +* See our [users wiki](https://github.com/reactphp/react/wiki/Users) and the + [dependents on Packagist](https://packagist.org/packages/react/event-loop/dependents) + for a list of packages that use the EventLoop in real-world applications. diff --git a/assets/php/vendor/react/event-loop/composer.json b/assets/php/vendor/react/event-loop/composer.json new file mode 100644 index 0000000..24974ec --- /dev/null +++ b/assets/php/vendor/react/event-loop/composer.json @@ -0,0 +1,21 @@ +{ + "name": "react/event-loop", + "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", + "keywords": ["event-loop", "asynchronous"], + "license": "MIT", + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.8.35 || ^5.7 || ^6.4" + }, + "suggest": { + "ext-event": "~1.0 for ExtEventLoop", + "ext-pcntl": "For signal handling support when using the StreamSelectLoop" + }, + "autoload": { + "psr-4": { + "React\\EventLoop\\": "src" + } + } +} diff --git a/assets/php/vendor/react/event-loop/examples/01-timers.php b/assets/php/vendor/react/event-loop/examples/01-timers.php new file mode 100644 index 0000000..e6107e4 --- /dev/null +++ b/assets/php/vendor/react/event-loop/examples/01-timers.php @@ -0,0 +1,15 @@ +<?php + +require __DIR__ . '/../vendor/autoload.php'; + +$loop = React\EventLoop\Factory::create(); + +$loop->addTimer(0.8, function () { + echo 'world!' . PHP_EOL; +}); + +$loop->addTimer(0.3, function () { + echo 'hello '; +}); + +$loop->run(); diff --git a/assets/php/vendor/react/event-loop/examples/02-periodic.php b/assets/php/vendor/react/event-loop/examples/02-periodic.php new file mode 100644 index 0000000..5e138a6 --- /dev/null +++ b/assets/php/vendor/react/event-loop/examples/02-periodic.php @@ -0,0 +1,16 @@ +<?php + +require __DIR__ . '/../vendor/autoload.php'; + +$loop = React\EventLoop\Factory::create(); + +$timer = $loop->addPeriodicTimer(0.1, function () { + echo 'tick!' . PHP_EOL; +}); + +$loop->addTimer(1.0, function () use ($loop, $timer) { + $loop->cancelTimer($timer); + echo 'Done' . PHP_EOL; +}); + +$loop->run(); diff --git a/assets/php/vendor/react/event-loop/examples/03-ticks.php b/assets/php/vendor/react/event-loop/examples/03-ticks.php new file mode 100644 index 0000000..3f36c6d --- /dev/null +++ b/assets/php/vendor/react/event-loop/examples/03-ticks.php @@ -0,0 +1,15 @@ +<?php + +require __DIR__ . '/../vendor/autoload.php'; + +$loop = React\EventLoop\Factory::create(); + +$loop->futureTick(function () { + echo 'b'; +}); +$loop->futureTick(function () { + echo 'c'; +}); +echo 'a'; + +$loop->run(); diff --git a/assets/php/vendor/react/event-loop/examples/04-signals.php b/assets/php/vendor/react/event-loop/examples/04-signals.php new file mode 100644 index 0000000..90b6898 --- /dev/null +++ b/assets/php/vendor/react/event-loop/examples/04-signals.php @@ -0,0 +1,19 @@ +<?php + +require __DIR__ . '/../vendor/autoload.php'; + +if (!defined('SIGINT')) { + fwrite(STDERR, 'Not supported on your platform (ext-pcntl missing or Windows?)' . PHP_EOL); + exit(1); +} + +$loop = React\EventLoop\Factory::create(); + +$loop->addSignal(SIGINT, $func = function ($signal) use ($loop, &$func) { + echo 'Signal: ', (string)$signal, PHP_EOL; + $loop->removeSignal(SIGINT, $func); +}); + +echo 'Listening for SIGINT. Use "kill -SIGINT ' . getmypid() . '" or CTRL+C' . PHP_EOL; + +$loop->run(); diff --git a/assets/php/vendor/react/event-loop/examples/11-consume-stdin.php b/assets/php/vendor/react/event-loop/examples/11-consume-stdin.php new file mode 100644 index 0000000..2a77245 --- /dev/null +++ b/assets/php/vendor/react/event-loop/examples/11-consume-stdin.php @@ -0,0 +1,30 @@ +<?php + +use React\EventLoop\Factory; + +require __DIR__ . '/../vendor/autoload.php'; + +if (!defined('STDIN') || stream_set_blocking(STDIN, false) !== true) { + fwrite(STDERR, 'ERROR: Unable to set STDIN non-blocking (not CLI or Windows?)' . PHP_EOL); + exit(1); +} + +$loop = Factory::create(); + +// read everything from STDIN and report number of bytes +// for illustration purposes only, should use react/stream instead +$loop->addReadStream(STDIN, function ($stream) use ($loop) { + $chunk = fread($stream, 64 * 1024); + + // reading nothing means we reached EOF + if ($chunk === '') { + $loop->removeReadStream($stream); + stream_set_blocking($stream, true); + fclose($stream); + return; + } + + echo strlen($chunk) . ' bytes' . PHP_EOL; +}); + +$loop->run(); diff --git a/assets/php/vendor/react/event-loop/examples/12-generate-yes.php b/assets/php/vendor/react/event-loop/examples/12-generate-yes.php new file mode 100644 index 0000000..ebc2beb --- /dev/null +++ b/assets/php/vendor/react/event-loop/examples/12-generate-yes.php @@ -0,0 +1,41 @@ +<?php + +require __DIR__ . '/../vendor/autoload.php'; + +// data can be given as first argument or defaults to "y" +$data = (isset($argv[1]) ? $argv[1] : 'y') . "\n"; + +// repeat data X times in order to fill around 200 KB +$data = str_repeat($data, round(200000 / strlen($data))); + +$loop = React\EventLoop\Factory::create(); + +if (!defined('STDOUT') || stream_set_blocking(STDOUT, false) !== true) { + fwrite(STDERR, 'ERROR: Unable to set STDOUT non-blocking (not CLI or Windows?)' . PHP_EOL); + exit(1); +} + +// write data to STDOUT whenever its write buffer accepts data +// for illustrations purpose only, should use react/stream instead +$loop->addWriteStream(STDOUT, function ($stdout) use ($loop, &$data) { + // try to write data + $r = fwrite($stdout, $data); + + // nothing could be written despite being writable => closed + if ($r === 0) { + $loop->removeWriteStream($stdout); + fclose($stdout); + stream_set_blocking($stdout, true); + fwrite(STDERR, 'Stopped because STDOUT closed' . PHP_EOL); + + return; + } + + // implement a very simple ring buffer, unless everything has been written at once: + // everything written in this iteration will be appended for next iteration + if (isset($data[$r])) { + $data = substr($data, $r) . substr($data, 0, $r); + } +}); + +$loop->run(); diff --git a/assets/php/vendor/react/event-loop/examples/13-http-client-blocking.php b/assets/php/vendor/react/event-loop/examples/13-http-client-blocking.php new file mode 100644 index 0000000..a2dde55 --- /dev/null +++ b/assets/php/vendor/react/event-loop/examples/13-http-client-blocking.php @@ -0,0 +1,35 @@ +<?php + +use React\EventLoop\Factory; + +require __DIR__ . '/../vendor/autoload.php'; + +$loop = Factory::create(); + +// connect to www.google.com:80 (blocking call!) +// for illustration purposes only, should use react/socket instead +$stream = stream_socket_client('tcp://www.google.com:80'); +if (!$stream) { + exit(1); +} +stream_set_blocking($stream, false); + +// send HTTP request +fwrite($stream, "GET / HTTP/1.1\r\nHost: www.google.com\r\nConnection: close\r\n\r\n"); + +// wait for HTTP response +$loop->addReadStream($stream, function ($stream) use ($loop) { + $chunk = fread($stream, 64 * 1024); + + // reading nothing means we reached EOF + if ($chunk === '') { + echo '[END]' . PHP_EOL; + $loop->removeReadStream($stream); + fclose($stream); + return; + } + + echo $chunk; +}); + +$loop->run(); diff --git a/assets/php/vendor/react/event-loop/examples/14-http-client-async.php b/assets/php/vendor/react/event-loop/examples/14-http-client-async.php new file mode 100644 index 0000000..c82c988 --- /dev/null +++ b/assets/php/vendor/react/event-loop/examples/14-http-client-async.php @@ -0,0 +1,63 @@ +<?php + +use React\EventLoop\Factory; + +require __DIR__ . '/../vendor/autoload.php'; + +$loop = Factory::create(); + +// resolve hostname before establishing TCP/IP connection (resolving DNS is still blocking here) +// for illustration purposes only, should use react/socket or react/dns instead! +$ip = gethostbyname('www.google.com'); +if (ip2long($ip) === false) { + echo 'Unable to resolve hostname' . PHP_EOL; + exit(1); +} + +// establish TCP/IP connection (non-blocking) +// for illustraction purposes only, should use react/socket instead! +$stream = stream_socket_client('tcp://' . $ip . ':80', $errno, $errstr, null, STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT); +if (!$stream) { + exit(1); +} +stream_set_blocking($stream, false); + +// print progress every 10ms +echo 'Connecting'; +$timer = $loop->addPeriodicTimer(0.01, function () { + echo '.'; +}); + +// wait for connection success/error +$loop->addWriteStream($stream, function ($stream) use ($loop, $timer) { + $loop->removeWriteStream($stream); + $loop->cancelTimer($timer); + + // check for socket error (connection rejected) + if (stream_socket_get_name($stream, true) === false) { + echo '[unable to connect]' . PHP_EOL; + exit(1); + } else { + echo '[connected]' . PHP_EOL; + } + + // send HTTP request + fwrite($stream, "GET / HTTP/1.1\r\nHost: www.google.com\r\nConnection: close\r\n\r\n"); + + // wait for HTTP response + $loop->addReadStream($stream, function ($stream) use ($loop) { + $chunk = fread($stream, 64 * 1024); + + // reading nothing means we reached EOF + if ($chunk === '') { + echo '[END]' . PHP_EOL; + $loop->removeReadStream($stream); + fclose($stream); + return; + } + + echo $chunk; + }); +}); + +$loop->run(); diff --git a/assets/php/vendor/react/event-loop/examples/21-http-server.php b/assets/php/vendor/react/event-loop/examples/21-http-server.php new file mode 100644 index 0000000..89520ce --- /dev/null +++ b/assets/php/vendor/react/event-loop/examples/21-http-server.php @@ -0,0 +1,36 @@ +<?php + +require __DIR__ . '/../vendor/autoload.php'; + +$loop = React\EventLoop\Factory::create(); + +// start TCP/IP server on localhost:8080 +// for illustration purposes only, should use react/socket instead +$server = stream_socket_server('tcp://127.0.0.1:8080'); +if (!$server) { + exit(1); +} +stream_set_blocking($server, false); + +// wait for incoming connections on server socket +$loop->addReadStream($server, function ($server) use ($loop) { + $conn = stream_socket_accept($server); + $data = "HTTP/1.1 200 OK\r\nContent-Length: 3\r\n\r\nHi\n"; + $loop->addWriteStream($conn, function ($conn) use (&$data, $loop) { + $written = fwrite($conn, $data); + if ($written === strlen($data)) { + fclose($conn); + $loop->removeWriteStream($conn); + } else { + $data = substr($data, $written); + } + }); +}); + +$loop->addPeriodicTimer(5, function () { + $memory = memory_get_usage() / 1024; + $formatted = number_format($memory, 3).'K'; + echo "Current memory usage: {$formatted}\n"; +}); + +$loop->run(); diff --git a/assets/php/vendor/react/event-loop/examples/91-benchmark-ticks.php b/assets/php/vendor/react/event-loop/examples/91-benchmark-ticks.php new file mode 100644 index 0000000..3f4690b --- /dev/null +++ b/assets/php/vendor/react/event-loop/examples/91-benchmark-ticks.php @@ -0,0 +1,15 @@ +<?php + +use React\EventLoop\Factory; + +require __DIR__ . '/../vendor/autoload.php'; + +$loop = Factory::create(); + +$n = isset($argv[1]) ? (int)$argv[1] : 1000 * 100; + +for ($i = 0; $i < $n; ++$i) { + $loop->futureTick(function () { }); +} + +$loop->run(); diff --git a/assets/php/vendor/react/event-loop/examples/92-benchmark-timers.php b/assets/php/vendor/react/event-loop/examples/92-benchmark-timers.php new file mode 100644 index 0000000..e2e02e4 --- /dev/null +++ b/assets/php/vendor/react/event-loop/examples/92-benchmark-timers.php @@ -0,0 +1,15 @@ +<?php + +use React\EventLoop\Factory; + +require __DIR__ . '/../vendor/autoload.php'; + +$loop = Factory::create(); + +$n = isset($argv[1]) ? (int)$argv[1] : 1000 * 100; + +for ($i = 0; $i < $n; ++$i) { + $loop->addTimer(0, function () { }); +} + +$loop->run(); diff --git a/assets/php/vendor/react/event-loop/examples/93-benchmark-ticks-delay.php b/assets/php/vendor/react/event-loop/examples/93-benchmark-ticks-delay.php new file mode 100644 index 0000000..95ee78c --- /dev/null +++ b/assets/php/vendor/react/event-loop/examples/93-benchmark-ticks-delay.php @@ -0,0 +1,22 @@ +<?php + +use React\EventLoop\Factory; + +require __DIR__ . '/../vendor/autoload.php'; + +$loop = Factory::create(); + +$ticks = isset($argv[1]) ? (int)$argv[1] : 1000 * 100; +$tick = function () use (&$tick, &$ticks, $loop) { + if ($ticks > 0) { + --$ticks; + //$loop->addTimer(0, $tick); + $loop->futureTick($tick); + } else { + echo 'done'; + } +}; + +$tick(); + +$loop->run(); diff --git a/assets/php/vendor/react/event-loop/examples/94-benchmark-timers-delay.php b/assets/php/vendor/react/event-loop/examples/94-benchmark-timers-delay.php new file mode 100644 index 0000000..2d6cfa2 --- /dev/null +++ b/assets/php/vendor/react/event-loop/examples/94-benchmark-timers-delay.php @@ -0,0 +1,22 @@ +<?php + +use React\EventLoop\Factory; + +require __DIR__ . '/../vendor/autoload.php'; + +$loop = Factory::create(); + +$ticks = isset($argv[1]) ? (int)$argv[1] : 1000 * 100; +$tick = function () use (&$tick, &$ticks, $loop) { + if ($ticks > 0) { + --$ticks; + //$loop->futureTick($tick); + $loop->addTimer(0, $tick); + } else { + echo 'done'; + } +}; + +$tick(); + +$loop->run(); diff --git a/assets/php/vendor/react/event-loop/examples/95-benchmark-memory.php b/assets/php/vendor/react/event-loop/examples/95-benchmark-memory.php new file mode 100644 index 0000000..084c404 --- /dev/null +++ b/assets/php/vendor/react/event-loop/examples/95-benchmark-memory.php @@ -0,0 +1,67 @@ +<?php + +/** + * Run the script indefinitely seconds with the loop from the factory and report every 2 seconds: + * php 95-benchmark-memory.php + * Run the script for 30 seconds with the stream_select loop and report every 10 seconds: + * php 95-benchmark-memory.php -t 30 -l StreamSelect -r 10 + */ + +use React\EventLoop\Factory; +use React\EventLoop\LoopInterface; +use React\EventLoop\TimerInterface; + +require __DIR__ . '/../vendor/autoload.php'; + +$args = getopt('t:l:r:'); +$t = isset($args['t']) ? (int)$args['t'] : 0; +$loop = isset($args['l']) && class_exists('React\EventLoop\\' . $args['l'] . 'Loop') ? 'React\EventLoop\\' . $args['l'] . 'Loop' : Factory::create(); + +if (!($loop instanceof LoopInterface)) { + $loop = new $loop(); +} + +$r = isset($args['r']) ? (int)$args['r'] : 2; + +$runs = 0; + +if (5 < $t) { + $loop->addTimer($t, function () use ($loop) { + $loop->stop(); + }); + +} + +$loop->addPeriodicTimer(0.001, function () use (&$runs, $loop) { + $runs++; + + $loop->addPeriodicTimer(1, function (TimerInterface $timer) use ($loop) { + $loop->cancelTimer($timer); + }); +}); + +$loop->addPeriodicTimer($r, function () use (&$runs) { + $kmem = round(memory_get_usage() / 1024); + $kmemReal = round(memory_get_usage(true) / 1024); + echo "Runs:\t\t\t$runs\n"; + echo "Memory (internal):\t$kmem KiB\n"; + echo "Memory (real):\t\t$kmemReal KiB\n"; + echo str_repeat('-', 50), "\n"; +}); + +echo "PHP Version:\t\t", phpversion(), "\n"; +echo "Loop\t\t\t", get_class($loop), "\n"; +echo "Time\t\t\t", date('r'), "\n"; + +echo str_repeat('-', 50), "\n"; + +$beginTime = time(); +$loop->run(); +$endTime = time(); +$timeTaken = $endTime - $beginTime; + +echo "PHP Version:\t\t", phpversion(), "\n"; +echo "Loop\t\t\t", get_class($loop), "\n"; +echo "Time\t\t\t", date('r'), "\n"; +echo "Time taken\t\t", $timeTaken, " seconds\n"; +echo "Runs per second\t\t", round($runs / $timeTaken), "\n"; diff --git a/assets/php/vendor/react/event-loop/phpunit.xml.dist b/assets/php/vendor/react/event-loop/phpunit.xml.dist new file mode 100644 index 0000000..cba6d4d --- /dev/null +++ b/assets/php/vendor/react/event-loop/phpunit.xml.dist @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<phpunit backupGlobals="false" + backupStaticAttributes="false" + colors="true" + convertErrorsToExceptions="true" + convertNoticesToExceptions="true" + convertWarningsToExceptions="true" + processIsolation="false" + stopOnFailure="false" + syntaxCheck="false" + bootstrap="tests/bootstrap.php" +> + <testsuites> + <testsuite name="React Test Suite"> + <directory>./tests/</directory> + </testsuite> + </testsuites> + + <filter> + <whitelist> + <directory>./src/</directory> + </whitelist> + </filter> +</phpunit> diff --git a/assets/php/vendor/react/event-loop/src/ExtEvLoop.php b/assets/php/vendor/react/event-loop/src/ExtEvLoop.php new file mode 100644 index 0000000..74db6d0 --- /dev/null +++ b/assets/php/vendor/react/event-loop/src/ExtEvLoop.php @@ -0,0 +1,252 @@ +<?php + +namespace React\EventLoop; + +use Ev; +use EvIo; +use EvLoop; +use React\EventLoop\Tick\FutureTickQueue; +use React\EventLoop\Timer\Timer; +use SplObjectStorage; + +/** + * An `ext-ev` based event loop. + * + * This loop uses the [`ev` PECL extension](https://pecl.php.net/package/ev), + * that provides an interface to `libev` library. + * + * This loop is known to work with PHP 5.4 through PHP 7+. + * + * @see http://php.net/manual/en/book.ev.php + * @see https://bitbucket.org/osmanov/pecl-ev/overview + */ +class ExtEvLoop implements LoopInterface +{ + /** + * @var EvLoop + */ + private $loop; + + /** + * @var FutureTickQueue + */ + private $futureTickQueue; + + /** + * @var SplObjectStorage + */ + private $timers; + + /** + * @var EvIo[] + */ + private $readStreams = []; + + /** + * @var EvIo[] + */ + private $writeStreams = []; + + /** + * @var bool + */ + private $running; + + /** + * @var SignalsHandler + */ + private $signals; + + /** + * @var \EvSignal[] + */ + private $signalEvents = []; + + public function __construct() + { + $this->loop = new EvLoop(); + $this->futureTickQueue = new FutureTickQueue(); + $this->timers = new SplObjectStorage(); + $this->signals = new SignalsHandler(); + } + + public function addReadStream($stream, $listener) + { + $key = (int)$stream; + + if (isset($this->readStreams[$key])) { + return; + } + + $callback = $this->getStreamListenerClosure($stream, $listener); + $event = $this->loop->io($stream, Ev::READ, $callback); + $this->readStreams[$key] = $event; + } + + /** + * @param resource $stream + * @param callable $listener + * + * @return \Closure + */ + private function getStreamListenerClosure($stream, $listener) + { + return function () use ($stream, $listener) { + call_user_func($listener, $stream); + }; + } + + public function addWriteStream($stream, $listener) + { + $key = (int)$stream; + + if (isset($this->writeStreams[$key])) { + return; + } + + $callback = $this->getStreamListenerClosure($stream, $listener); + $event = $this->loop->io($stream, Ev::WRITE, $callback); + $this->writeStreams[$key] = $event; + } + + public function removeReadStream($stream) + { + $key = (int)$stream; + + if (!isset($this->readStreams[$key])) { + return; + } + + $this->readStreams[$key]->stop(); + unset($this->readStreams[$key]); + } + + public function removeWriteStream($stream) + { + $key = (int)$stream; + + if (!isset($this->writeStreams[$key])) { + return; + } + + $this->writeStreams[$key]->stop(); + unset($this->writeStreams[$key]); + } + + public function addTimer($interval, $callback) + { + $timer = new Timer($interval, $callback, false); + + $that = $this; + $timers = $this->timers; + $callback = function () use ($timer, $timers, $that) { + call_user_func($timer->getCallback(), $timer); + + if ($timers->contains($timer)) { + $that->cancelTimer($timer); + } + }; + + $event = $this->loop->timer($timer->getInterval(), 0.0, $callback); + $this->timers->attach($timer, $event); + + return $timer; + } + + public function addPeriodicTimer($interval, $callback) + { + $timer = new Timer($interval, $callback, true); + + $callback = function () use ($timer) { + call_user_func($timer->getCallback(), $timer); + }; + + $event = $this->loop->timer($interval, $interval, $callback); + $this->timers->attach($timer, $event); + + return $timer; + } + + public function cancelTimer(TimerInterface $timer) + { + if (!isset($this->timers[$timer])) { + return; + } + + $event = $this->timers[$timer]; + $event->stop(); + $this->timers->detach($timer); + } + + public function futureTick($listener) + { + $this->futureTickQueue->add($listener); + } + + public function run() + { + $this->running = true; + + while ($this->running) { + $this->futureTickQueue->tick(); + + $hasPendingCallbacks = !$this->futureTickQueue->isEmpty(); + $wasJustStopped = !$this->running; + $nothingLeftToDo = !$this->readStreams + && !$this->writeStreams + && !$this->timers->count() + && $this->signals->isEmpty(); + + $flags = Ev::RUN_ONCE; + if ($wasJustStopped || $hasPendingCallbacks) { + $flags |= Ev::RUN_NOWAIT; + } elseif ($nothingLeftToDo) { + break; + } + + $this->loop->run($flags); + } + } + + public function stop() + { + $this->running = false; + } + + public function __destruct() + { + /** @var TimerInterface $timer */ + foreach ($this->timers as $timer) { + $this->cancelTimer($timer); + } + + foreach ($this->readStreams as $key => $stream) { + $this->removeReadStream($key); + } + + foreach ($this->writeStreams as $key => $stream) { + $this->removeWriteStream($key); + } + } + + public function addSignal($signal, $listener) + { + $this->signals->add($signal, $listener); + + if (!isset($this->signalEvents[$signal])) { + $this->signalEvents[$signal] = $this->loop->signal($signal, function() use ($signal) { + $this->signals->call($signal); + }); + } + } + + public function removeSignal($signal, $listener) + { + $this->signals->remove($signal, $listener); + + if (isset($this->signalEvents[$signal])) { + $this->signalEvents[$signal]->stop(); + unset($this->signalEvents[$signal]); + } + } +} diff --git a/assets/php/vendor/react/event-loop/src/ExtEventLoop.php b/assets/php/vendor/react/event-loop/src/ExtEventLoop.php new file mode 100644 index 0000000..622dd47 --- /dev/null +++ b/assets/php/vendor/react/event-loop/src/ExtEventLoop.php @@ -0,0 +1,259 @@ +<?php + +namespace React\EventLoop; + +use BadMethodCallException; +use Event; +use EventBase; +use EventConfig as EventBaseConfig; +use React\EventLoop\Tick\FutureTickQueue; +use React\EventLoop\Timer\Timer; +use SplObjectStorage; + +/** + * An `ext-event` based event loop. + * + * This uses the [`event` PECL extension](https://pecl.php.net/package/event). + * It supports the same backends as libevent. + * + * This loop is known to work with PHP 5.4 through PHP 7+. + * + * @link https://pecl.php.net/package/event + */ +final class ExtEventLoop implements LoopInterface +{ + private $eventBase; + private $futureTickQueue; + private $timerCallback; + private $timerEvents; + private $streamCallback; + private $readEvents = array(); + private $writeEvents = array(); + private $readListeners = array(); + private $writeListeners = array(); + private $readRefs = array(); + private $writeRefs = array(); + private $running; + private $signals; + private $signalEvents = array(); + + public function __construct() + { + if (!class_exists('EventBase', false)) { + throw new BadMethodCallException('Cannot create ExtEventLoop, ext-event extension missing'); + } + + $config = new EventBaseConfig(); + $config->requireFeatures(EventBaseConfig::FEATURE_FDS); + + $this->eventBase = new EventBase($config); + $this->futureTickQueue = new FutureTickQueue(); + $this->timerEvents = new SplObjectStorage(); + $this->signals = new SignalsHandler(); + + $this->createTimerCallback(); + $this->createStreamCallback(); + } + + public function addReadStream($stream, $listener) + { + $key = (int) $stream; + if (isset($this->readListeners[$key])) { + return; + } + + $event = new Event($this->eventBase, $stream, Event::PERSIST | Event::READ, $this->streamCallback); + $event->add(); + $this->readEvents[$key] = $event; + $this->readListeners[$key] = $listener; + + // ext-event does not increase refcount on stream resources for PHP 7+ + // manually keep track of stream resource to prevent premature garbage collection + if (PHP_VERSION_ID >= 70000) { + $this->readRefs[$key] = $stream; + } + } + + public function addWriteStream($stream, $listener) + { + $key = (int) $stream; + if (isset($this->writeListeners[$key])) { + return; + } + + $event = new Event($this->eventBase, $stream, Event::PERSIST | Event::WRITE, $this->streamCallback); + $event->add(); + $this->writeEvents[$key] = $event; + $this->writeListeners[$key] = $listener; + + // ext-event does not increase refcount on stream resources for PHP 7+ + // manually keep track of stream resource to prevent premature garbage collection + if (PHP_VERSION_ID >= 70000) { + $this->writeRefs[$key] = $stream; + } + } + + public function removeReadStream($stream) + { + $key = (int) $stream; + + if (isset($this->readEvents[$key])) { + $this->readEvents[$key]->free(); + unset( + $this->readEvents[$key], + $this->readListeners[$key], + $this->readRefs[$key] + ); + } + } + + public function removeWriteStream($stream) + { + $key = (int) $stream; + + if (isset($this->writeEvents[$key])) { + $this->writeEvents[$key]->free(); + unset( + $this->writeEvents[$key], + $this->writeListeners[$key], + $this->writeRefs[$key] + ); + } + } + + public function addTimer($interval, $callback) + { + $timer = new Timer($interval, $callback, false); + + $this->scheduleTimer($timer); + + return $timer; + } + + public function addPeriodicTimer($interval, $callback) + { + $timer = new Timer($interval, $callback, true); + + $this->scheduleTimer($timer); + + return $timer; + } + + public function cancelTimer(TimerInterface $timer) + { + if ($this->timerEvents->contains($timer)) { + $this->timerEvents[$timer]->free(); + $this->timerEvents->detach($timer); + } + } + + public function futureTick($listener) + { + $this->futureTickQueue->add($listener); + } + + public function addSignal($signal, $listener) + { + $this->signals->add($signal, $listener); + + if (!isset($this->signalEvents[$signal])) { + $this->signalEvents[$signal] = Event::signal($this->eventBase, $signal, array($this->signals, 'call')); + $this->signalEvents[$signal]->add(); + } + } + + public function removeSignal($signal, $listener) + { + $this->signals->remove($signal, $listener); + + if (isset($this->signalEvents[$signal]) && $this->signals->count($signal) === 0) { + $this->signalEvents[$signal]->free(); + unset($this->signalEvents[$signal]); + } + } + + public function run() + { + $this->running = true; + + while ($this->running) { + $this->futureTickQueue->tick(); + + $flags = EventBase::LOOP_ONCE; + if (!$this->running || !$this->futureTickQueue->isEmpty()) { + $flags |= EventBase::LOOP_NONBLOCK; + } elseif (!$this->readEvents && !$this->writeEvents && !$this->timerEvents->count() && $this->signals->isEmpty()) { + break; + } + + $this->eventBase->loop($flags); + } + } + + public function stop() + { + $this->running = false; + } + + /** + * Schedule a timer for execution. + * + * @param TimerInterface $timer + */ + private function scheduleTimer(TimerInterface $timer) + { + $flags = Event::TIMEOUT; + + if ($timer->isPeriodic()) { + $flags |= Event::PERSIST; + } + + $event = new Event($this->eventBase, -1, $flags, $this->timerCallback, $timer); + $this->timerEvents[$timer] = $event; + + $event->add($timer->getInterval()); + } + + /** + * Create a callback used as the target of timer events. + * + * A reference is kept to the callback for the lifetime of the loop + * to prevent "Cannot destroy active lambda function" fatal error from + * the event extension. + */ + private function createTimerCallback() + { + $timers = $this->timerEvents; + $this->timerCallback = function ($_, $__, $timer) use ($timers) { + call_user_func($timer->getCallback(), $timer); + + if (!$timer->isPeriodic() && $timers->contains($timer)) { + $this->cancelTimer($timer); + } + }; + } + + /** + * Create a callback used as the target of stream events. + * + * A reference is kept to the callback for the lifetime of the loop + * to prevent "Cannot destroy active lambda function" fatal error from + * the event extension. + */ + private function createStreamCallback() + { + $read =& $this->readListeners; + $write =& $this->writeListeners; + $this->streamCallback = function ($stream, $flags) use (&$read, &$write) { + $key = (int) $stream; + + if (Event::READ === (Event::READ & $flags) && isset($read[$key])) { + call_user_func($read[$key], $stream); + } + + if (Event::WRITE === (Event::WRITE & $flags) && isset($write[$key])) { + call_user_func($write[$key], $stream); + } + }; + } +} diff --git a/assets/php/vendor/react/event-loop/src/ExtLibevLoop.php b/assets/php/vendor/react/event-loop/src/ExtLibevLoop.php new file mode 100644 index 0000000..d3b0df8 --- /dev/null +++ b/assets/php/vendor/react/event-loop/src/ExtLibevLoop.php @@ -0,0 +1,199 @@ +<?php + +namespace React\EventLoop; + +use BadMethodCallException; +use libev\EventLoop; +use libev\IOEvent; +use libev\SignalEvent; +use libev\TimerEvent; +use React\EventLoop\Tick\FutureTickQueue; +use React\EventLoop\Timer\Timer; +use SplObjectStorage; + +/** + * An `ext-libev` based event loop. + * + * This uses an [unofficial `libev` extension](https://github.com/m4rw3r/php-libev). + * It supports the same backends as libevent. + * + * This loop does only work with PHP 5. + * An update for PHP 7 is [unlikely](https://github.com/m4rw3r/php-libev/issues/8) + * to happen any time soon. + * + * @see https://github.com/m4rw3r/php-libev + * @see https://gist.github.com/1688204 + */ +final class ExtLibevLoop implements LoopInterface +{ + private $loop; + private $futureTickQueue; + private $timerEvents; + private $readEvents = array(); + private $writeEvents = array(); + private $running; + private $signals; + private $signalEvents = array(); + + public function __construct() + { + if (!class_exists('libev\EventLoop', false)) { + throw new BadMethodCallException('Cannot create ExtLibevLoop, ext-libev extension missing'); + } + + $this->loop = new EventLoop(); + $this->futureTickQueue = new FutureTickQueue(); + $this->timerEvents = new SplObjectStorage(); + $this->signals = new SignalsHandler(); + } + + public function addReadStream($stream, $listener) + { + if (isset($this->readEvents[(int) $stream])) { + return; + } + + $callback = function () use ($stream, $listener) { + call_user_func($listener, $stream); + }; + + $event = new IOEvent($callback, $stream, IOEvent::READ); + $this->loop->add($event); + + $this->readEvents[(int) $stream] = $event; + } + + public function addWriteStream($stream, $listener) + { + if (isset($this->writeEvents[(int) $stream])) { + return; + } + + $callback = function () use ($stream, $listener) { + call_user_func($listener, $stream); + }; + + $event = new IOEvent($callback, $stream, IOEvent::WRITE); + $this->loop->add($event); + + $this->writeEvents[(int) $stream] = $event; + } + + public function removeReadStream($stream) + { + $key = (int) $stream; + + if (isset($this->readEvents[$key])) { + $this->readEvents[$key]->stop(); + $this->loop->remove($this->readEvents[$key]); + unset($this->readEvents[$key]); + } + } + + public function removeWriteStream($stream) + { + $key = (int) $stream; + + if (isset($this->writeEvents[$key])) { + $this->writeEvents[$key]->stop(); + $this->loop->remove($this->writeEvents[$key]); + unset($this->writeEvents[$key]); + } + } + + public function addTimer($interval, $callback) + { + $timer = new Timer( $interval, $callback, false); + + $that = $this; + $timers = $this->timerEvents; + $callback = function () use ($timer, $timers, $that) { + call_user_func($timer->getCallback(), $timer); + + if ($timers->contains($timer)) { + $that->cancelTimer($timer); + } + }; + + $event = new TimerEvent($callback, $timer->getInterval()); + $this->timerEvents->attach($timer, $event); + $this->loop->add($event); + + return $timer; + } + + public function addPeriodicTimer($interval, $callback) + { + $timer = new Timer($interval, $callback, true); + + $callback = function () use ($timer) { + call_user_func($timer->getCallback(), $timer); + }; + + $event = new TimerEvent($callback, $interval, $interval); + $this->timerEvents->attach($timer, $event); + $this->loop->add($event); + + return $timer; + } + + public function cancelTimer(TimerInterface $timer) + { + if (isset($this->timerEvents[$timer])) { + $this->loop->remove($this->timerEvents[$timer]); + $this->timerEvents->detach($timer); + } + } + + public function futureTick($listener) + { + $this->futureTickQueue->add($listener); + } + + public function addSignal($signal, $listener) + { + $this->signals->add($signal, $listener); + + if (!isset($this->signalEvents[$signal])) { + $signals = $this->signals; + $this->signalEvents[$signal] = new SignalEvent(function () use ($signals, $signal) { + $signals->call($signal); + }, $signal); + $this->loop->add($this->signalEvents[$signal]); + } + } + + public function removeSignal($signal, $listener) + { + $this->signals->remove($signal, $listener); + + if (isset($this->signalEvents[$signal]) && $this->signals->count($signal) === 0) { + $this->signalEvents[$signal]->stop(); + $this->loop->remove($this->signalEvents[$signal]); + unset($this->signalEvents[$signal]); + } + } + + public function run() + { + $this->running = true; + + while ($this->running) { + $this->futureTickQueue->tick(); + + $flags = EventLoop::RUN_ONCE; + if (!$this->running || !$this->futureTickQueue->isEmpty()) { + $flags |= EventLoop::RUN_NOWAIT; + } elseif (!$this->readEvents && !$this->writeEvents && !$this->timerEvents->count() && $this->signals->isEmpty()) { + break; + } + + $this->loop->run($flags); + } + } + + public function stop() + { + $this->running = false; + } +} diff --git a/assets/php/vendor/react/event-loop/src/ExtLibeventLoop.php b/assets/php/vendor/react/event-loop/src/ExtLibeventLoop.php new file mode 100644 index 0000000..427f8db --- /dev/null +++ b/assets/php/vendor/react/event-loop/src/ExtLibeventLoop.php @@ -0,0 +1,283 @@ +<?php + +namespace React\EventLoop; + +use BadMethodCallException; +use Event; +use EventBase; +use React\EventLoop\Tick\FutureTickQueue; +use React\EventLoop\Timer\Timer; +use SplObjectStorage; + +/** + * An `ext-libevent` based event loop. + * + * This uses the [`libevent` PECL extension](https://pecl.php.net/package/libevent). + * `libevent` itself supports a number of system-specific backends (epoll, kqueue). + * + * This event loop does only work with PHP 5. + * An [unofficial update](https://github.com/php/pecl-event-libevent/pull/2) for + * PHP 7 does exist, but it is known to cause regular crashes due to `SEGFAULT`s. + * To reiterate: Using this event loop on PHP 7 is not recommended. + * Accordingly, the [`Factory`](#factory) will not try to use this event loop on + * PHP 7. + * + * This event loop is known to trigger a readable listener only if + * the stream *becomes* readable (edge-triggered) and may not trigger if the + * stream has already been readable from the beginning. + * This also implies that a stream may not be recognized as readable when data + * is still left in PHP's internal stream buffers. + * As such, it's recommended to use `stream_set_read_buffer($stream, 0);` + * to disable PHP's internal read buffer in this case. + * See also [`addReadStream()`](#addreadstream) for more details. + * + * @link https://pecl.php.net/package/libevent + */ +final class ExtLibeventLoop implements LoopInterface +{ + /** @internal */ + const MICROSECONDS_PER_SECOND = 1000000; + + private $eventBase; + private $futureTickQueue; + private $timerCallback; + private $timerEvents; + private $streamCallback; + private $readEvents = array(); + private $writeEvents = array(); + private $readListeners = array(); + private $writeListeners = array(); + private $running; + private $signals; + private $signalEvents = array(); + + public function __construct() + { + if (!function_exists('event_base_new')) { + throw new BadMethodCallException('Cannot create ExtLibeventLoop, ext-libevent extension missing'); + } + + $this->eventBase = event_base_new(); + $this->futureTickQueue = new FutureTickQueue(); + $this->timerEvents = new SplObjectStorage(); + $this->signals = new SignalsHandler(); + + $this->createTimerCallback(); + $this->createStreamCallback(); + } + + public function addReadStream($stream, $listener) + { + $key = (int) $stream; + if (isset($this->readListeners[$key])) { + return; + } + + $event = event_new(); + event_set($event, $stream, EV_PERSIST | EV_READ, $this->streamCallback); + event_base_set($event, $this->eventBase); + event_add($event); + + $this->readEvents[$key] = $event; + $this->readListeners[$key] = $listener; + } + + public function addWriteStream($stream, $listener) + { + $key = (int) $stream; + if (isset($this->writeListeners[$key])) { + return; + } + + $event = event_new(); + event_set($event, $stream, EV_PERSIST | EV_WRITE, $this->streamCallback); + event_base_set($event, $this->eventBase); + event_add($event); + + $this->writeEvents[$key] = $event; + $this->writeListeners[$key] = $listener; + } + + public function removeReadStream($stream) + { + $key = (int) $stream; + + if (isset($this->readListeners[$key])) { + $event = $this->readEvents[$key]; + event_del($event); + event_free($event); + + unset( + $this->readEvents[$key], + $this->readListeners[$key] + ); + } + } + + public function removeWriteStream($stream) + { + $key = (int) $stream; + + if (isset($this->writeListeners[$key])) { + $event = $this->writeEvents[$key]; + event_del($event); + event_free($event); + + unset( + $this->writeEvents[$key], + $this->writeListeners[$key] + ); + } + } + + public function addTimer($interval, $callback) + { + $timer = new Timer($interval, $callback, false); + + $this->scheduleTimer($timer); + + return $timer; + } + + public function addPeriodicTimer($interval, $callback) + { + $timer = new Timer($interval, $callback, true); + + $this->scheduleTimer($timer); + + return $timer; + } + + public function cancelTimer(TimerInterface $timer) + { + if ($this->timerEvents->contains($timer)) { + $event = $this->timerEvents[$timer]; + event_del($event); + event_free($event); + + $this->timerEvents->detach($timer); + } + } + + public function futureTick($listener) + { + $this->futureTickQueue->add($listener); + } + + public function addSignal($signal, $listener) + { + $this->signals->add($signal, $listener); + + if (!isset($this->signalEvents[$signal])) { + $this->signalEvents[$signal] = event_new(); + event_set($this->signalEvents[$signal], $signal, EV_PERSIST | EV_SIGNAL, array($this->signals, 'call')); + event_base_set($this->signalEvents[$signal], $this->eventBase); + event_add($this->signalEvents[$signal]); + } + } + + public function removeSignal($signal, $listener) + { + $this->signals->remove($signal, $listener); + + if (isset($this->signalEvents[$signal]) && $this->signals->count($signal) === 0) { + event_del($this->signalEvents[$signal]); + event_free($this->signalEvents[$signal]); + unset($this->signalEvents[$signal]); + } + } + + public function run() + { + $this->running = true; + + while ($this->running) { + $this->futureTickQueue->tick(); + + $flags = EVLOOP_ONCE; + if (!$this->running || !$this->futureTickQueue->isEmpty()) { + $flags |= EVLOOP_NONBLOCK; + } elseif (!$this->readEvents && !$this->writeEvents && !$this->timerEvents->count() && $this->signals->isEmpty()) { + break; + } + + event_base_loop($this->eventBase, $flags); + } + } + + public function stop() + { + $this->running = false; + } + + /** + * Schedule a timer for execution. + * + * @param TimerInterface $timer + */ + private function scheduleTimer(TimerInterface $timer) + { + $this->timerEvents[$timer] = $event = event_timer_new(); + + event_timer_set($event, $this->timerCallback, $timer); + event_base_set($event, $this->eventBase); + event_add($event, $timer->getInterval() * self::MICROSECONDS_PER_SECOND); + } + + /** + * Create a callback used as the target of timer events. + * + * A reference is kept to the callback for the lifetime of the loop + * to prevent "Cannot destroy active lambda function" fatal error from + * the event extension. + */ + private function createTimerCallback() + { + $that = $this; + $timers = $this->timerEvents; + $this->timerCallback = function ($_, $__, $timer) use ($timers, $that) { + call_user_func($timer->getCallback(), $timer); + + // Timer already cancelled ... + if (!$timers->contains($timer)) { + return; + } + + // Reschedule periodic timers ... + if ($timer->isPeriodic()) { + event_add( + $timers[$timer], + $timer->getInterval() * ExtLibeventLoop::MICROSECONDS_PER_SECOND + ); + + // Clean-up one shot timers ... + } else { + $that->cancelTimer($timer); + } + }; + } + + /** + * Create a callback used as the target of stream events. + * + * A reference is kept to the callback for the lifetime of the loop + * to prevent "Cannot destroy active lambda function" fatal error from + * the event extension. + */ + private function createStreamCallback() + { + $read =& $this->readListeners; + $write =& $this->writeListeners; + $this->streamCallback = function ($stream, $flags) use (&$read, &$write) { + $key = (int) $stream; + + if (EV_READ === (EV_READ & $flags) && isset($read[$key])) { + call_user_func($read[$key], $stream); + } + + if (EV_WRITE === (EV_WRITE & $flags) && isset($write[$key])) { + call_user_func($write[$key], $stream); + } + }; + } +} diff --git a/assets/php/vendor/react/event-loop/src/Factory.php b/assets/php/vendor/react/event-loop/src/Factory.php new file mode 100644 index 0000000..b46fc07 --- /dev/null +++ b/assets/php/vendor/react/event-loop/src/Factory.php @@ -0,0 +1,41 @@ +<?php + +namespace React\EventLoop; + +/** + * The `Factory` class exists as a convenient way to pick the best available event loop implementation. + */ +final class Factory +{ + /** + * Creates a new event loop instance + * + * ```php + * $loop = React\EventLoop\Factory::create(); + * ``` + * + * This method always returns an instance implementing `LoopInterface`, + * the actual event loop implementation is an implementation detail. + * + * This method should usually only be called once at the beginning of the program. + * + * @return LoopInterface + */ + public static function create() + { + // @codeCoverageIgnoreStart + if (class_exists('libev\EventLoop', false)) { + return new ExtLibevLoop(); + } elseif (class_exists('EvLoop', false)) { + return new ExtEvLoop(); + } elseif (class_exists('EventBase', false)) { + return new ExtEventLoop(); + } elseif (function_exists('event_base_new') && PHP_VERSION_ID < 70000) { + // only use ext-libevent on PHP < 7 for now + return new ExtLibeventLoop(); + } + + return new StreamSelectLoop(); + // @codeCoverageIgnoreEnd + } +} diff --git a/assets/php/vendor/react/event-loop/src/LoopInterface.php b/assets/php/vendor/react/event-loop/src/LoopInterface.php new file mode 100644 index 0000000..1cc8640 --- /dev/null +++ b/assets/php/vendor/react/event-loop/src/LoopInterface.php @@ -0,0 +1,463 @@ +<?php + +namespace React\EventLoop; + +interface LoopInterface +{ + /** + * [Advanced] Register a listener to be notified when a stream is ready to read. + * + * Note that this low-level API is considered advanced usage. + * Most use cases should probably use the higher-level + * [readable Stream API](https://github.com/reactphp/stream#readablestreaminterface) + * instead. + * + * The first parameter MUST be a valid stream resource that supports + * checking whether it is ready to read by this loop implementation. + * A single stream resource MUST NOT be added more than once. + * Instead, either call [`removeReadStream()`](#removereadstream) first or + * react to this event with a single listener and then dispatch from this + * listener. This method MAY throw an `Exception` if the given resource type + * is not supported by this loop implementation. + * + * The listener callback function MUST be able to accept a single parameter, + * the stream resource added by this method or you MAY use a function which + * has no parameters at all. + * + * The listener callback function MUST NOT throw an `Exception`. + * The return value of the listener callback function will be ignored and has + * no effect, so for performance reasons you're recommended to not return + * any excessive data structures. + * + * If you want to access any variables within your callback function, you + * can bind arbitrary data to a callback closure like this: + * + * ```php + * $loop->addReadStream($stream, function ($stream) use ($name) { + * echo $name . ' said: ' . fread($stream); + * }); + * ``` + * + * See also [example #11](examples). + * + * You can invoke [`removeReadStream()`](#removereadstream) to remove the + * read event listener for this stream. + * + * The execution order of listeners when multiple streams become ready at + * the same time is not guaranteed. + * + * @param resource $stream The PHP stream resource to check. + * @param callable $listener Invoked when the stream is ready. + * @throws \Exception if the given resource type is not supported by this loop implementation + * @see self::removeReadStream() + */ + public function addReadStream($stream, $listener); + + /** + * [Advanced] Register a listener to be notified when a stream is ready to write. + * + * Note that this low-level API is considered advanced usage. + * Most use cases should probably use the higher-level + * [writable Stream API](https://github.com/reactphp/stream#writablestreaminterface) + * instead. + * + * The first parameter MUST be a valid stream resource that supports + * checking whether it is ready to write by this loop implementation. + * A single stream resource MUST NOT be added more than once. + * Instead, either call [`removeWriteStream()`](#removewritestream) first or + * react to this event with a single listener and then dispatch from this + * listener. This method MAY throw an `Exception` if the given resource type + * is not supported by this loop implementation. + * + * The listener callback function MUST be able to accept a single parameter, + * the stream resource added by this method or you MAY use a function which + * has no parameters at all. + * + * The listener callback function MUST NOT throw an `Exception`. + * The return value of the listener callback function will be ignored and has + * no effect, so for performance reasons you're recommended to not return + * any excessive data structures. + * + * If you want to access any variables within your callback function, you + * can bind arbitrary data to a callback closure like this: + * + * ```php + * $loop->addWriteStream($stream, function ($stream) use ($name) { + * fwrite($stream, 'Hello ' . $name); + * }); + * ``` + * + * See also [example #12](examples). + * + * You can invoke [`removeWriteStream()`](#removewritestream) to remove the + * write event listener for this stream. + * + * The execution order of listeners when multiple streams become ready at + * the same time is not guaranteed. + * + * Some event loop implementations are known to only trigger the listener if + * the stream *becomes* readable (edge-triggered) and may not trigger if the + * stream has already been readable from the beginning. + * This also implies that a stream may not be recognized as readable when data + * is still left in PHP's internal stream buffers. + * As such, it's recommended to use `stream_set_read_buffer($stream, 0);` + * to disable PHP's internal read buffer in this case. + * + * @param resource $stream The PHP stream resource to check. + * @param callable $listener Invoked when the stream is ready. + * @throws \Exception if the given resource type is not supported by this loop implementation + * @see self::removeWriteStream() + */ + public function addWriteStream($stream, $listener); + + /** + * Remove the read event listener for the given stream. + * + * Removing a stream from the loop that has already been removed or trying + * to remove a stream that was never added or is invalid has no effect. + * + * @param resource $stream The PHP stream resource. + */ + public function removeReadStream($stream); + + /** + * Remove the write event listener for the given stream. + * + * Removing a stream from the loop that has already been removed or trying + * to remove a stream that was never added or is invalid has no effect. + * + * @param resource $stream The PHP stream resource. + */ + public function removeWriteStream($stream); + + /** + * Enqueue a callback to be invoked once after the given interval. + * + * The timer callback function MUST be able to accept a single parameter, + * the timer instance as also returned by this method or you MAY use a + * function which has no parameters at all. + * + * The timer callback function MUST NOT throw an `Exception`. + * The return value of the timer callback function will be ignored and has + * no effect, so for performance reasons you're recommended to not return + * any excessive data structures. + * + * Unlike [`addPeriodicTimer()`](#addperiodictimer), this method will ensure + * the callback will be invoked only once after the given interval. + * You can invoke [`cancelTimer`](#canceltimer) to cancel a pending timer. + * + * ```php + * $loop->addTimer(0.8, function () { + * echo 'world!' . PHP_EOL; + * }); + * + * $loop->addTimer(0.3, function () { + * echo 'hello '; + * }); + * ``` + * + * See also [example #1](examples). + * + * If you want to access any variables within your callback function, you + * can bind arbitrary data to a callback closure like this: + * + * ```php + * function hello($name, LoopInterface $loop) + * { + * $loop->addTimer(1.0, function () use ($name) { + * echo "hello $name\n"; + * }); + * } + * + * hello('Tester', $loop); + * ``` + * + * This interface does not enforce any particular timer resolution, so + * special care may have to be taken if you rely on very high precision with + * millisecond accuracy or below. Event loop implementations SHOULD work on + * a best effort basis and SHOULD provide at least millisecond accuracy + * unless otherwise noted. Many existing event loop implementations are + * known to provide microsecond accuracy, but it's generally not recommended + * to rely on this high precision. + * + * Similarly, the execution order of timers scheduled to execute at the + * same time (within its possible accuracy) is not guaranteed. + * + * This interface suggests that event loop implementations SHOULD use a + * monotonic time source if available. Given that a monotonic time source is + * not available on PHP by default, event loop implementations MAY fall back + * to using wall-clock time. + * While this does not affect many common use cases, this is an important + * distinction for programs that rely on a high time precision or on systems + * that are subject to discontinuous time adjustments (time jumps). + * This means that if you schedule a timer to trigger in 30s and then adjust + * your system time forward by 20s, the timer SHOULD still trigger in 30s. + * See also [event loop implementations](#loop-implementations) for more details. + * + * @param int|float $interval The number of seconds to wait before execution. + * @param callable $callback The callback to invoke. + * + * @return TimerInterface + */ + public function addTimer($interval, $callback); + + /** + * Enqueue a callback to be invoked repeatedly after the given interval. + * + * The timer callback function MUST be able to accept a single parameter, + * the timer instance as also returned by this method or you MAY use a + * function which has no parameters at all. + * + * The timer callback function MUST NOT throw an `Exception`. + * The return value of the timer callback function will be ignored and has + * no effect, so for performance reasons you're recommended to not return + * any excessive data structures. + * + * Unlike [`addTimer()`](#addtimer), this method will ensure the the + * callback will be invoked infinitely after the given interval or until you + * invoke [`cancelTimer`](#canceltimer). + * + * ```php + * $timer = $loop->addPeriodicTimer(0.1, function () { + * echo 'tick!' . PHP_EOL; + * }); + * + * $loop->addTimer(1.0, function () use ($loop, $timer) { + * $loop->cancelTimer($timer); + * echo 'Done' . PHP_EOL; + * }); + * ``` + * + * See also [example #2](examples). + * + * If you want to limit the number of executions, you can bind + * arbitrary data to a callback closure like this: + * + * ```php + * function hello($name, LoopInterface $loop) + * { + * $n = 3; + * $loop->addPeriodicTimer(1.0, function ($timer) use ($name, $loop, &$n) { + * if ($n > 0) { + * --$n; + * echo "hello $name\n"; + * } else { + * $loop->cancelTimer($timer); + * } + * }); + * } + * + * hello('Tester', $loop); + * ``` + * + * This interface does not enforce any particular timer resolution, so + * special care may have to be taken if you rely on very high precision with + * millisecond accuracy or below. Event loop implementations SHOULD work on + * a best effort basis and SHOULD provide at least millisecond accuracy + * unless otherwise noted. Many existing event loop implementations are + * known to provide microsecond accuracy, but it's generally not recommended + * to rely on this high precision. + * + * Similarly, the execution order of timers scheduled to execute at the + * same time (within its possible accuracy) is not guaranteed. + * + * This interface suggests that event loop implementations SHOULD use a + * monotonic time source if available. Given that a monotonic time source is + * not available on PHP by default, event loop implementations MAY fall back + * to using wall-clock time. + * While this does not affect many common use cases, this is an important + * distinction for programs that rely on a high time precision or on systems + * that are subject to discontinuous time adjustments (time jumps). + * This means that if you schedule a timer to trigger in 30s and then adjust + * your system time forward by 20s, the timer SHOULD still trigger in 30s. + * See also [event loop implementations](#loop-implementations) for more details. + * + * Additionally, periodic timers may be subject to timer drift due to + * re-scheduling after each invocation. As such, it's generally not + * recommended to rely on this for high precision intervals with millisecond + * accuracy or below. + * + * @param int|float $interval The number of seconds to wait before execution. + * @param callable $callback The callback to invoke. + * + * @return TimerInterface + */ + public function addPeriodicTimer($interval, $callback); + + /** + * Cancel a pending timer. + * + * See also [`addPeriodicTimer()`](#addperiodictimer) and [example #2](examples). + * + * Calling this method on a timer instance that has not been added to this + * loop instance or on a timer that has already been cancelled has no effect. + * + * @param TimerInterface $timer The timer to cancel. + * + * @return void + */ + public function cancelTimer(TimerInterface $timer); + + /** + * Schedule a callback to be invoked on a future tick of the event loop. + * + * This works very much similar to timers with an interval of zero seconds, + * but does not require the overhead of scheduling a timer queue. + * + * The tick callback function MUST be able to accept zero parameters. + * + * The tick callback function MUST NOT throw an `Exception`. + * The return value of the tick callback function will be ignored and has + * no effect, so for performance reasons you're recommended to not return + * any excessive data structures. + * + * If you want to access any variables within your callback function, you + * can bind arbitrary data to a callback closure like this: + * + * ```php + * function hello($name, LoopInterface $loop) + * { + * $loop->futureTick(function () use ($name) { + * echo "hello $name\n"; + * }); + * } + * + * hello('Tester', $loop); + * ``` + * + * Unlike timers, tick callbacks are guaranteed to be executed in the order + * they are enqueued. + * Also, once a callback is enqueued, there's no way to cancel this operation. + * + * This is often used to break down bigger tasks into smaller steps (a form + * of cooperative multitasking). + * + * ```php + * $loop->futureTick(function () { + * echo 'b'; + * }); + * $loop->futureTick(function () { + * echo 'c'; + * }); + * echo 'a'; + * ``` + * + * See also [example #3](examples). + * + * @param callable $listener The callback to invoke. + * + * @return void + */ + public function futureTick($listener); + + /** + * Register a listener to be notified when a signal has been caught by this process. + * + * This is useful to catch user interrupt signals or shutdown signals from + * tools like `supervisor` or `systemd`. + * + * The listener callback function MUST be able to accept a single parameter, + * the signal added by this method or you MAY use a function which + * has no parameters at all. + * + * The listener callback function MUST NOT throw an `Exception`. + * The return value of the listener callback function will be ignored and has + * no effect, so for performance reasons you're recommended to not return + * any excessive data structures. + * + * ```php + * $loop->addSignal(SIGINT, function (int $signal) { + * echo 'Caught user interrupt signal' . PHP_EOL; + * }); + * ``` + * + * See also [example #4](examples). + * + * Signaling is only available on Unix-like platform, Windows isn't + * supported due to operating system limitations. + * This method may throw a `BadMethodCallException` if signals aren't + * supported on this platform, for example when required extensions are + * missing. + * + * **Note: A listener can only be added once to the same signal, any + * attempts to add it more then once will be ignored.** + * + * @param int $signal + * @param callable $listener + * + * @throws \BadMethodCallException when signals aren't supported on this + * platform, for example when required extensions are missing. + * + * @return void + */ + public function addSignal($signal, $listener); + + /** + * Removes a previously added signal listener. + * + * ```php + * $loop->removeSignal(SIGINT, $listener); + * ``` + * + * Any attempts to remove listeners that aren't registered will be ignored. + * + * @param int $signal + * @param callable $listener + * + * @return void + */ + public function removeSignal($signal, $listener); + + /** + * Run the event loop until there are no more tasks to perform. + * + * For many applications, this method is the only directly visible + * invocation on the event loop. + * As a rule of thumb, it is usally recommended to attach everything to the + * same loop instance and then run the loop once at the bottom end of the + * application. + * + * ```php + * $loop->run(); + * ``` + * + * This method will keep the loop running until there are no more tasks + * to perform. In other words: This method will block until the last + * timer, stream and/or signal has been removed. + * + * Likewise, it is imperative to ensure the application actually invokes + * this method once. Adding listeners to the loop and missing to actually + * run it will result in the application exiting without actually waiting + * for any of the attached listeners. + * + * This method MUST NOT be called while the loop is already running. + * This method MAY be called more than once after it has explicity been + * [`stop()`ped](#stop) or after it automatically stopped because it + * previously did no longer have anything to do. + * + * @return void + */ + public function run(); + + /** + * Instruct a running event loop to stop. + * + * This method is considered advanced usage and should be used with care. + * As a rule of thumb, it is usually recommended to let the loop stop + * only automatically when it no longer has anything to do. + * + * This method can be used to explicitly instruct the event loop to stop: + * + * ```php + * $loop->addTimer(3.0, function () use ($loop) { + * $loop->stop(); + * }); + * ``` + * + * Calling this method on a loop instance that is not currently running or + * on a loop instance that has already been stopped has no effect. + * + * @return void + */ + public function stop(); +} diff --git a/assets/php/vendor/react/event-loop/src/SignalsHandler.php b/assets/php/vendor/react/event-loop/src/SignalsHandler.php new file mode 100644 index 0000000..523e1ca --- /dev/null +++ b/assets/php/vendor/react/event-loop/src/SignalsHandler.php @@ -0,0 +1,63 @@ +<?php + +namespace React\EventLoop; + +/** + * @internal + */ +final class SignalsHandler +{ + private $signals = array(); + + public function add($signal, $listener) + { + if (!isset($this->signals[$signal])) { + $this->signals[$signal] = array(); + } + + if (in_array($listener, $this->signals[$signal])) { + return; + } + + $this->signals[$signal][] = $listener; + } + + public function remove($signal, $listener) + { + if (!isset($this->signals[$signal])) { + return; + } + + $index = \array_search($listener, $this->signals[$signal], true); + unset($this->signals[$signal][$index]); + + if (isset($this->signals[$signal]) && \count($this->signals[$signal]) === 0) { + unset($this->signals[$signal]); + } + } + + public function call($signal) + { + if (!isset($this->signals[$signal])) { + return; + } + + foreach ($this->signals[$signal] as $listener) { + \call_user_func($listener, $signal); + } + } + + public function count($signal) + { + if (!isset($this->signals[$signal])) { + return 0; + } + + return \count($this->signals[$signal]); + } + + public function isEmpty() + { + return !$this->signals; + } +} diff --git a/assets/php/vendor/react/event-loop/src/StreamSelectLoop.php b/assets/php/vendor/react/event-loop/src/StreamSelectLoop.php new file mode 100644 index 0000000..e82e9e4 --- /dev/null +++ b/assets/php/vendor/react/event-loop/src/StreamSelectLoop.php @@ -0,0 +1,275 @@ +<?php + +namespace React\EventLoop; + +use React\EventLoop\Signal\Pcntl; +use React\EventLoop\Tick\FutureTickQueue; +use React\EventLoop\Timer\Timer; +use React\EventLoop\Timer\Timers; + +/** + * A `stream_select()` based event loop. + * + * This uses the [`stream_select()`](http://php.net/manual/en/function.stream-select.php) + * function and is the only implementation which works out of the box with PHP. + * + * This event loop works out of the box on PHP 5.4 through PHP 7+ and HHVM. + * This means that no installation is required and this library works on all + * platforms and supported PHP versions. + * Accordingly, the [`Factory`](#factory) will use this event loop by default if + * you do not install any of the event loop extensions listed below. + * + * Under the hood, it does a simple `select` system call. + * This system call is limited to the maximum file descriptor number of + * `FD_SETSIZE` (platform dependent, commonly 1024) and scales with `O(m)` + * (`m` being the maximum file descriptor number passed). + * This means that you may run into issues when handling thousands of streams + * concurrently and you may want to look into using one of the alternative + * event loop implementations listed below in this case. + * If your use case is among the many common use cases that involve handling only + * dozens or a few hundred streams at once, then this event loop implementation + * performs really well. + * + * If you want to use signal handling (see also [`addSignal()`](#addsignal) below), + * this event loop implementation requires `ext-pcntl`. + * This extension is only available for Unix-like platforms and does not support + * Windows. + * It is commonly installed as part of many PHP distributions. + * If this extension is missing (or you're running on Windows), signal handling is + * not supported and throws a `BadMethodCallException` instead. + * + * This event loop is known to rely on wall-clock time to schedule future + * timers, because a monotonic time source is not available in PHP by default. + * While this does not affect many common use cases, this is an important + * distinction for programs that rely on a high time precision or on systems + * that are subject to discontinuous time adjustments (time jumps). + * This means that if you schedule a timer to trigger in 30s and then adjust + * your system time forward by 20s, the timer may trigger in 10s. + * See also [`addTimer()`](#addtimer) for more details. + * + * @link http://php.net/manual/en/function.stream-select.php + */ +final class StreamSelectLoop implements LoopInterface +{ + /** @internal */ + const MICROSECONDS_PER_SECOND = 1000000; + + private $futureTickQueue; + private $timers; + private $readStreams = array(); + private $readListeners = array(); + private $writeStreams = array(); + private $writeListeners = array(); + private $running; + private $pcntl = false; + private $signals; + + public function __construct() + { + $this->futureTickQueue = new FutureTickQueue(); + $this->timers = new Timers(); + $this->pcntl = extension_loaded('pcntl'); + $this->signals = new SignalsHandler(); + } + + public function addReadStream($stream, $listener) + { + $key = (int) $stream; + + if (!isset($this->readStreams[$key])) { + $this->readStreams[$key] = $stream; + $this->readListeners[$key] = $listener; + } + } + + public function addWriteStream($stream, $listener) + { + $key = (int) $stream; + + if (!isset($this->writeStreams[$key])) { + $this->writeStreams[$key] = $stream; + $this->writeListeners[$key] = $listener; + } + } + + public function removeReadStream($stream) + { + $key = (int) $stream; + + unset( + $this->readStreams[$key], + $this->readListeners[$key] + ); + } + + public function removeWriteStream($stream) + { + $key = (int) $stream; + + unset( + $this->writeStreams[$key], + $this->writeListeners[$key] + ); + } + + public function addTimer($interval, $callback) + { + $timer = new Timer($interval, $callback, false); + + $this->timers->add($timer); + + return $timer; + } + + public function addPeriodicTimer($interval, $callback) + { + $timer = new Timer($interval, $callback, true); + + $this->timers->add($timer); + + return $timer; + } + + public function cancelTimer(TimerInterface $timer) + { + $this->timers->cancel($timer); + } + + public function futureTick($listener) + { + $this->futureTickQueue->add($listener); + } + + public function addSignal($signal, $listener) + { + if ($this->pcntl === false) { + throw new \BadMethodCallException('Event loop feature "signals" isn\'t supported by the "StreamSelectLoop"'); + } + + $first = $this->signals->count($signal) === 0; + $this->signals->add($signal, $listener); + + if ($first) { + \pcntl_signal($signal, array($this->signals, 'call')); + } + } + + public function removeSignal($signal, $listener) + { + if (!$this->signals->count($signal)) { + return; + } + + $this->signals->remove($signal, $listener); + + if ($this->signals->count($signal) === 0) { + \pcntl_signal($signal, SIG_DFL); + } + } + + public function run() + { + $this->running = true; + + while ($this->running) { + $this->futureTickQueue->tick(); + + $this->timers->tick(); + + // Future-tick queue has pending callbacks ... + if (!$this->running || !$this->futureTickQueue->isEmpty()) { + $timeout = 0; + + // There is a pending timer, only block until it is due ... + } elseif ($scheduledAt = $this->timers->getFirst()) { + $timeout = $scheduledAt - $this->timers->getTime(); + if ($timeout < 0) { + $timeout = 0; + } else { + // Convert float seconds to int microseconds. + // Ensure we do not exceed maximum integer size, which may + // cause the loop to tick once every ~35min on 32bit systems. + $timeout *= self::MICROSECONDS_PER_SECOND; + $timeout = $timeout > PHP_INT_MAX ? PHP_INT_MAX : (int)$timeout; + } + + // The only possible event is stream or signal activity, so wait forever ... + } elseif ($this->readStreams || $this->writeStreams || !$this->signals->isEmpty()) { + $timeout = null; + + // There's nothing left to do ... + } else { + break; + } + + $this->waitForStreamActivity($timeout); + } + } + + public function stop() + { + $this->running = false; + } + + /** + * Wait/check for stream activity, or until the next timer is due. + * + * @param integer|null $timeout Activity timeout in microseconds, or null to wait forever. + */ + private function waitForStreamActivity($timeout) + { + $read = $this->readStreams; + $write = $this->writeStreams; + + $available = $this->streamSelect($read, $write, $timeout); + if ($this->pcntl) { + \pcntl_signal_dispatch(); + } + if (false === $available) { + // if a system call has been interrupted, + // we cannot rely on it's outcome + return; + } + + foreach ($read as $stream) { + $key = (int) $stream; + + if (isset($this->readListeners[$key])) { + call_user_func($this->readListeners[$key], $stream); + } + } + + foreach ($write as $stream) { + $key = (int) $stream; + + if (isset($this->writeListeners[$key])) { + call_user_func($this->writeListeners[$key], $stream); + } + } + } + + /** + * Emulate a stream_select() implementation that does not break when passed + * empty stream arrays. + * + * @param array &$read An array of read streams to select upon. + * @param array &$write An array of write streams to select upon. + * @param integer|null $timeout Activity timeout in microseconds, or null to wait forever. + * + * @return integer|false The total number of streams that are ready for read/write. + * Can return false if stream_select() is interrupted by a signal. + */ + private function streamSelect(array &$read, array &$write, $timeout) + { + if ($read || $write) { + $except = null; + + // suppress warnings that occur, when stream_select is interrupted by a signal + return @stream_select($read, $write, $except, $timeout === null ? null : 0, $timeout); + } + + $timeout && usleep($timeout); + + return 0; + } +} diff --git a/assets/php/vendor/react/event-loop/src/Tick/FutureTickQueue.php b/assets/php/vendor/react/event-loop/src/Tick/FutureTickQueue.php new file mode 100644 index 0000000..c79afc5 --- /dev/null +++ b/assets/php/vendor/react/event-loop/src/Tick/FutureTickQueue.php @@ -0,0 +1,60 @@ +<?php + +namespace React\EventLoop\Tick; + +use SplQueue; + +/** + * A tick queue implementation that can hold multiple callback functions + * + * This class should only be used internally, see LoopInterface instead. + * + * @see LoopInterface + * @internal + */ +final class FutureTickQueue +{ + private $queue; + + public function __construct() + { + $this->queue = new SplQueue(); + } + + /** + * Add a callback to be invoked on a future tick of the event loop. + * + * Callbacks are guaranteed to be executed in the order they are enqueued. + * + * @param callable $listener The callback to invoke. + */ + public function add($listener) + { + $this->queue->enqueue($listener); + } + + /** + * Flush the callback queue. + */ + public function tick() + { + // Only invoke as many callbacks as were on the queue when tick() was called. + $count = $this->queue->count(); + + while ($count--) { + call_user_func( + $this->queue->dequeue() + ); + } + } + + /** + * Check if the next tick queue is empty. + * + * @return boolean + */ + public function isEmpty() + { + return $this->queue->isEmpty(); + } +} diff --git a/assets/php/vendor/react/event-loop/src/Timer/Timer.php b/assets/php/vendor/react/event-loop/src/Timer/Timer.php new file mode 100644 index 0000000..da3602a --- /dev/null +++ b/assets/php/vendor/react/event-loop/src/Timer/Timer.php @@ -0,0 +1,55 @@ +<?php + +namespace React\EventLoop\Timer; + +use React\EventLoop\TimerInterface; + +/** + * The actual connection implementation for TimerInterface + * + * This class should only be used internally, see TimerInterface instead. + * + * @see TimerInterface + * @internal + */ +final class Timer implements TimerInterface +{ + const MIN_INTERVAL = 0.000001; + + private $interval; + private $callback; + private $periodic; + + /** + * Constructor initializes the fields of the Timer + * + * @param float $interval The interval after which this timer will execute, in seconds + * @param callable $callback The callback that will be executed when this timer elapses + * @param bool $periodic Whether the time is periodic + */ + public function __construct($interval, $callback, $periodic = false) + { + if ($interval < self::MIN_INTERVAL) { + $interval = self::MIN_INTERVAL; + } + + $this->interval = (float) $interval; + $this->callback = $callback; + $this->periodic = (bool) $periodic; + } + + public function getInterval() + { + return $this->interval; + } + + public function getCallback() + { + return $this->callback; + } + + public function isPeriodic() + { + return $this->periodic; + } +} diff --git a/assets/php/vendor/react/event-loop/src/Timer/Timers.php b/assets/php/vendor/react/event-loop/src/Timer/Timers.php new file mode 100644 index 0000000..17bbdac --- /dev/null +++ b/assets/php/vendor/react/event-loop/src/Timer/Timers.php @@ -0,0 +1,109 @@ +<?php + +namespace React\EventLoop\Timer; + +use React\EventLoop\TimerInterface; +use SplObjectStorage; +use SplPriorityQueue; + +/** + * A scheduler implementation that can hold multiple timer instances + * + * This class should only be used internally, see TimerInterface instead. + * + * @see TimerInterface + * @internal + */ +final class Timers +{ + private $time; + private $timers; + private $scheduler; + + public function __construct() + { + $this->timers = new SplObjectStorage(); + $this->scheduler = new SplPriorityQueue(); + } + + public function updateTime() + { + return $this->time = microtime(true); + } + + public function getTime() + { + return $this->time ?: $this->updateTime(); + } + + public function add(TimerInterface $timer) + { + $interval = $timer->getInterval(); + $scheduledAt = $interval + microtime(true); + + $this->timers->attach($timer, $scheduledAt); + $this->scheduler->insert($timer, -$scheduledAt); + } + + public function contains(TimerInterface $timer) + { + return $this->timers->contains($timer); + } + + public function cancel(TimerInterface $timer) + { + $this->timers->detach($timer); + } + + public function getFirst() + { + while ($this->scheduler->count()) { + $timer = $this->scheduler->top(); + + if ($this->timers->contains($timer)) { + return $this->timers[$timer]; + } + + $this->scheduler->extract(); + } + + return null; + } + + public function isEmpty() + { + return count($this->timers) === 0; + } + + public function tick() + { + $time = $this->updateTime(); + $timers = $this->timers; + $scheduler = $this->scheduler; + + while (!$scheduler->isEmpty()) { + $timer = $scheduler->top(); + + if (!isset($timers[$timer])) { + $scheduler->extract(); + $timers->detach($timer); + + continue; + } + + if ($timers[$timer] >= $time) { + break; + } + + $scheduler->extract(); + call_user_func($timer->getCallback(), $timer); + + if ($timer->isPeriodic() && isset($timers[$timer])) { + $timers[$timer] = $scheduledAt = $timer->getInterval() + $time; + $scheduler->insert($timer, -$scheduledAt); + } else { + $timers->detach($timer); + } + } + } +} diff --git a/assets/php/vendor/react/event-loop/src/TimerInterface.php b/assets/php/vendor/react/event-loop/src/TimerInterface.php new file mode 100644 index 0000000..cdcf773 --- /dev/null +++ b/assets/php/vendor/react/event-loop/src/TimerInterface.php @@ -0,0 +1,27 @@ +<?php + +namespace React\EventLoop; + +interface TimerInterface +{ + /** + * Get the interval after which this timer will execute, in seconds + * + * @return float + */ + public function getInterval(); + + /** + * Get the callback that will be executed when this timer elapses + * + * @return callable + */ + public function getCallback(); + + /** + * Determine whether the time is periodic + * + * @return bool + */ + public function isPeriodic(); +} diff --git a/assets/php/vendor/react/event-loop/tests/AbstractLoopTest.php b/assets/php/vendor/react/event-loop/tests/AbstractLoopTest.php new file mode 100644 index 0000000..dbfc91e --- /dev/null +++ b/assets/php/vendor/react/event-loop/tests/AbstractLoopTest.php @@ -0,0 +1,621 @@ +<?php + +namespace React\Tests\EventLoop; + +abstract class AbstractLoopTest extends TestCase +{ + /** + * @var \React\EventLoop\LoopInterface + */ + protected $loop; + + private $tickTimeout; + + const PHP_DEFAULT_CHUNK_SIZE = 8192; + + public function setUp() + { + // It's a timeout, don't set it too low. Travis and other CI systems are slow. + $this->tickTimeout = 0.02; + $this->loop = $this->createLoop(); + } + + abstract public function createLoop(); + + public function createSocketPair() + { + $domain = (DIRECTORY_SEPARATOR === '\\') ? STREAM_PF_INET : STREAM_PF_UNIX; + $sockets = stream_socket_pair($domain, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP); + + foreach ($sockets as $socket) { + if (function_exists('stream_set_read_buffer')) { + stream_set_read_buffer($socket, 0); + } + } + + return $sockets; + } + + public function testAddReadStream() + { + list ($input, $output) = $this->createSocketPair(); + + $this->loop->addReadStream($input, $this->expectCallableExactly(2)); + + fwrite($output, "foo\n"); + $this->tickLoop($this->loop); + + fwrite($output, "bar\n"); + $this->tickLoop($this->loop); + } + + public function testAddReadStreamIgnoresSecondCallable() + { + list ($input, $output) = $this->createSocketPair(); + + $this->loop->addReadStream($input, $this->expectCallableExactly(2)); + $this->loop->addReadStream($input, $this->expectCallableNever()); + + fwrite($output, "foo\n"); + $this->tickLoop($this->loop); + + fwrite($output, "bar\n"); + $this->tickLoop($this->loop); + } + + public function testAddReadStreamReceivesDataFromStreamReference() + { + $this->received = ''; + $this->subAddReadStreamReceivesDataFromStreamReference(); + $this->assertEquals('', $this->received); + + $this->assertRunFasterThan($this->tickTimeout * 2); + $this->assertEquals('[hello]X', $this->received); + } + + /** + * Helper for above test. This happens in another helper method to verify + * the loop keeps track of assigned stream resources (refcount). + */ + private function subAddReadStreamReceivesDataFromStreamReference() + { + list ($input, $output) = $this->createSocketPair(); + + fwrite($input, 'hello'); + fclose($input); + + $loop = $this->loop; + $received =& $this->received; + $loop->addReadStream($output, function ($output) use ($loop, &$received) { + $chunk = fread($output, 1024); + if ($chunk === '') { + $received .= 'X'; + $loop->removeReadStream($output); + fclose($output); + } else { + $received .= '[' . $chunk . ']'; + } + }); + } + + public function testAddWriteStream() + { + list ($input) = $this->createSocketPair(); + + $this->loop->addWriteStream($input, $this->expectCallableExactly(2)); + $this->tickLoop($this->loop); + $this->tickLoop($this->loop); + } + + public function testAddWriteStreamIgnoresSecondCallable() + { + list ($input) = $this->createSocketPair(); + + $this->loop->addWriteStream($input, $this->expectCallableExactly(2)); + $this->loop->addWriteStream($input, $this->expectCallableNever()); + $this->tickLoop($this->loop); + $this->tickLoop($this->loop); + } + + public function testRemoveReadStreamInstantly() + { + list ($input, $output) = $this->createSocketPair(); + + $this->loop->addReadStream($input, $this->expectCallableNever()); + $this->loop->removeReadStream($input); + + fwrite($output, "bar\n"); + $this->tickLoop($this->loop); + } + + public function testRemoveReadStreamAfterReading() + { + list ($input, $output) = $this->createSocketPair(); + + $this->loop->addReadStream($input, $this->expectCallableOnce()); + + fwrite($output, "foo\n"); + $this->tickLoop($this->loop); + + $this->loop->removeReadStream($input); + + fwrite($output, "bar\n"); + $this->tickLoop($this->loop); + } + + public function testRemoveWriteStreamInstantly() + { + list ($input) = $this->createSocketPair(); + + $this->loop->addWriteStream($input, $this->expectCallableNever()); + $this->loop->removeWriteStream($input); + $this->tickLoop($this->loop); + } + + public function testRemoveWriteStreamAfterWriting() + { + list ($input) = $this->createSocketPair(); + + $this->loop->addWriteStream($input, $this->expectCallableOnce()); + $this->tickLoop($this->loop); + + $this->loop->removeWriteStream($input); + $this->tickLoop($this->loop); + } + + public function testRemoveStreamForReadOnly() + { + list ($input, $output) = $this->createSocketPair(); + + $this->loop->addReadStream($input, $this->expectCallableNever()); + $this->loop->addWriteStream($output, $this->expectCallableOnce()); + $this->loop->removeReadStream($input); + + fwrite($output, "foo\n"); + $this->tickLoop($this->loop); + } + + public function testRemoveStreamForWriteOnly() + { + list ($input, $output) = $this->createSocketPair(); + + fwrite($output, "foo\n"); + + $this->loop->addReadStream($input, $this->expectCallableOnce()); + $this->loop->addWriteStream($output, $this->expectCallableNever()); + $this->loop->removeWriteStream($output); + + $this->tickLoop($this->loop); + } + + public function testRemoveReadAndWriteStreamFromLoopOnceResourceClosesEndsLoop() + { + list($stream, $other) = $this->createSocketPair(); + stream_set_blocking($stream, false); + stream_set_blocking($other, false); + + // dummy writable handler + $this->loop->addWriteStream($stream, function () { }); + + // remove stream when the stream is readable (closes) + $loop = $this->loop; + $loop->addReadStream($stream, function ($stream) use ($loop) { + $loop->removeReadStream($stream); + $loop->removeWriteStream($stream); + fclose($stream); + }); + + // close other side + fclose($other); + + $this->assertRunFasterThan($this->tickTimeout); + } + + public function testRemoveReadAndWriteStreamFromLoopOnceResourceClosesOnEndOfFileEndsLoop() + { + list($stream, $other) = $this->createSocketPair(); + stream_set_blocking($stream, false); + stream_set_blocking($other, false); + + // dummy writable handler + $this->loop->addWriteStream($stream, function () { }); + + // remove stream when the stream is readable (closes) + $loop = $this->loop; + $loop->addReadStream($stream, function ($stream) use ($loop) { + $data = fread($stream, 1024); + if ($data !== '') { + return; + } + + $loop->removeReadStream($stream); + $loop->removeWriteStream($stream); + fclose($stream); + }); + + // send data and close stream + fwrite($other, str_repeat('.', static::PHP_DEFAULT_CHUNK_SIZE)); + $this->loop->addTimer(0.01, function () use ($other) { + fclose($other); + }); + + $this->assertRunFasterThan(0.1); + } + + public function testRemoveReadAndWriteStreamFromLoopWithClosingResourceEndsLoop() + { + // get only one part of the pair to ensure the other side will close immediately + list($stream) = $this->createSocketPair(); + stream_set_blocking($stream, false); + + // dummy writable handler + $this->loop->addWriteStream($stream, function () { }); + + // remove stream when the stream is readable (closes) + $loop = $this->loop; + $loop->addReadStream($stream, function ($stream) use ($loop) { + $loop->removeReadStream($stream); + $loop->removeWriteStream($stream); + fclose($stream); + }); + + $this->assertRunFasterThan($this->tickTimeout); + } + + public function testRemoveInvalid() + { + list ($stream) = $this->createSocketPair(); + + // remove a valid stream from the event loop that was never added in the first place + $this->loop->removeReadStream($stream); + $this->loop->removeWriteStream($stream); + + $this->assertTrue(true); + } + + /** @test */ + public function emptyRunShouldSimplyReturn() + { + $this->assertRunFasterThan($this->tickTimeout); + } + + /** @test */ + public function runShouldReturnWhenNoMoreFds() + { + list ($input, $output) = $this->createSocketPair(); + + $loop = $this->loop; + $this->loop->addReadStream($input, function ($stream) use ($loop) { + $loop->removeReadStream($stream); + }); + + fwrite($output, "foo\n"); + + $this->assertRunFasterThan($this->tickTimeout * 2); + } + + /** @test */ + public function stopShouldStopRunningLoop() + { + list ($input, $output) = $this->createSocketPair(); + + $loop = $this->loop; + $this->loop->addReadStream($input, function ($stream) use ($loop) { + $loop->stop(); + }); + + fwrite($output, "foo\n"); + + $this->assertRunFasterThan($this->tickTimeout * 2); + } + + public function testStopShouldPreventRunFromBlocking() + { + $that = $this; + $this->loop->addTimer( + 1, + function () use ($that) { + $that->fail('Timer was executed.'); + } + ); + + $loop = $this->loop; + $this->loop->futureTick( + function () use ($loop) { + $loop->stop(); + } + ); + + $this->assertRunFasterThan($this->tickTimeout * 2); + } + + public function testIgnoreRemovedCallback() + { + // two independent streams, both should be readable right away + list ($input1, $output1) = $this->createSocketPair(); + list ($input2, $output2) = $this->createSocketPair(); + + $called = false; + + $loop = $this->loop; + $loop->addReadStream($input1, function ($stream) use (& $called, $loop, $input2) { + // stream1 is readable, remove stream2 as well => this will invalidate its callback + $loop->removeReadStream($stream); + $loop->removeReadStream($input2); + + $called = true; + }); + + // this callback would have to be called as well, but the first stream already removed us + $that = $this; + $loop->addReadStream($input2, function () use (& $called, $that) { + if ($called) { + $that->fail('Callback 2 must not be called after callback 1 was called'); + } + }); + + fwrite($output1, "foo\n"); + fwrite($output2, "foo\n"); + + $loop->run(); + + $this->assertTrue($called); + } + + public function testFutureTickEventGeneratedByFutureTick() + { + $loop = $this->loop; + $this->loop->futureTick( + function () use ($loop) { + $loop->futureTick( + function () { + echo 'future-tick' . PHP_EOL; + } + ); + } + ); + + $this->expectOutputString('future-tick' . PHP_EOL); + + $this->loop->run(); + } + + public function testFutureTick() + { + $called = false; + + $callback = function () use (&$called) { + $called = true; + }; + + $this->loop->futureTick($callback); + + $this->assertFalse($called); + + $this->tickLoop($this->loop); + + $this->assertTrue($called); + } + + public function testFutureTickFiresBeforeIO() + { + list ($stream) = $this->createSocketPair(); + + $this->loop->addWriteStream( + $stream, + function () { + echo 'stream' . PHP_EOL; + } + ); + + $this->loop->futureTick( + function () { + echo 'future-tick' . PHP_EOL; + } + ); + + $this->expectOutputString('future-tick' . PHP_EOL . 'stream' . PHP_EOL); + + $this->tickLoop($this->loop); + } + + public function testRecursiveFutureTick() + { + list ($stream) = $this->createSocketPair(); + + $loop = $this->loop; + $this->loop->addWriteStream( + $stream, + function () use ($stream, $loop) { + echo 'stream' . PHP_EOL; + $loop->removeWriteStream($stream); + } + ); + + $this->loop->futureTick( + function () use ($loop) { + echo 'future-tick-1' . PHP_EOL; + $loop->futureTick( + function () { + echo 'future-tick-2' . PHP_EOL; + } + ); + } + ); + + $this->expectOutputString('future-tick-1' . PHP_EOL . 'stream' . PHP_EOL . 'future-tick-2' . PHP_EOL); + + $this->loop->run(); + } + + public function testRunWaitsForFutureTickEvents() + { + list ($stream) = $this->createSocketPair(); + + $loop = $this->loop; + $this->loop->addWriteStream( + $stream, + function () use ($stream, $loop) { + $loop->removeWriteStream($stream); + $loop->futureTick( + function () { + echo 'future-tick' . PHP_EOL; + } + ); + } + ); + + $this->expectOutputString('future-tick' . PHP_EOL); + + $this->loop->run(); + } + + public function testFutureTickEventGeneratedByTimer() + { + $loop = $this->loop; + $this->loop->addTimer( + 0.001, + function () use ($loop) { + $loop->futureTick( + function () { + echo 'future-tick' . PHP_EOL; + } + ); + } + ); + + $this->expectOutputString('future-tick' . PHP_EOL); + + $this->loop->run(); + } + + public function testRemoveSignalNotRegisteredIsNoOp() + { + $this->loop->removeSignal(SIGINT, function () { }); + $this->assertTrue(true); + } + + public function testSignal() + { + if (!function_exists('posix_kill') || !function_exists('posix_getpid')) { + $this->markTestSkipped('Signal test skipped because functions "posix_kill" and "posix_getpid" are missing.'); + } + + $called = false; + $calledShouldNot = true; + + $timer = $this->loop->addPeriodicTimer(1, function () {}); + + $this->loop->addSignal(SIGUSR2, $func2 = function () use (&$calledShouldNot) { + $calledShouldNot = false; + }); + + $loop = $this->loop; + $this->loop->addSignal(SIGUSR1, $func1 = function () use (&$func1, &$func2, &$called, $timer, $loop) { + $called = true; + $loop->removeSignal(SIGUSR1, $func1); + $loop->removeSignal(SIGUSR2, $func2); + $loop->cancelTimer($timer); + }); + + $this->loop->futureTick(function () { + posix_kill(posix_getpid(), SIGUSR1); + }); + + $this->loop->run(); + + $this->assertTrue($called); + $this->assertTrue($calledShouldNot); + } + + public function testSignalMultipleUsagesForTheSameListener() + { + $funcCallCount = 0; + $func = function () use (&$funcCallCount) { + $funcCallCount++; + }; + $this->loop->addTimer(1, function () {}); + + $this->loop->addSignal(SIGUSR1, $func); + $this->loop->addSignal(SIGUSR1, $func); + + $this->loop->addTimer(0.4, function () { + posix_kill(posix_getpid(), SIGUSR1); + }); + $loop = $this->loop; + $this->loop->addTimer(0.9, function () use (&$func, $loop) { + $loop->removeSignal(SIGUSR1, $func); + }); + + $this->loop->run(); + + $this->assertSame(1, $funcCallCount); + } + + public function testSignalsKeepTheLoopRunning() + { + $loop = $this->loop; + $function = function () {}; + $this->loop->addSignal(SIGUSR1, $function); + $this->loop->addTimer(1.5, function () use ($function, $loop) { + $loop->removeSignal(SIGUSR1, $function); + $loop->stop(); + }); + + $this->assertRunSlowerThan(1.5); + } + + public function testSignalsKeepTheLoopRunningAndRemovingItStopsTheLoop() + { + $loop = $this->loop; + $function = function () {}; + $this->loop->addSignal(SIGUSR1, $function); + $this->loop->addTimer(1.5, function () use ($function, $loop) { + $loop->removeSignal(SIGUSR1, $function); + }); + + $this->assertRunFasterThan(1.6); + } + + public function testTimerIntervalCanBeFarInFuture() + { + // get only one part of the pair to ensure the other side will close immediately + list($stream) = $this->createSocketPair(); + + // start a timer very far in the future + $timer = $this->loop->addTimer(PHP_INT_MAX, function () { }); + + // remove stream and timer when the stream is readable (closes) + $loop = $this->loop; + $this->loop->addReadStream($stream, function ($stream) use ($timer, $loop) { + $loop->removeReadStream($stream); + $loop->cancelTimer($timer); + }); + + $this->assertRunFasterThan($this->tickTimeout); + } + + private function assertRunSlowerThan($minInterval) + { + $start = microtime(true); + + $this->loop->run(); + + $end = microtime(true); + $interval = $end - $start; + + $this->assertLessThan($interval, $minInterval); + } + + private function assertRunFasterThan($maxInterval) + { + $start = microtime(true); + + $this->loop->run(); + + $end = microtime(true); + $interval = $end - $start; + + $this->assertLessThan($maxInterval, $interval); + } +} diff --git a/assets/php/vendor/react/event-loop/tests/CallableStub.php b/assets/php/vendor/react/event-loop/tests/CallableStub.php new file mode 100644 index 0000000..913d403 --- /dev/null +++ b/assets/php/vendor/react/event-loop/tests/CallableStub.php @@ -0,0 +1,10 @@ +<?php + +namespace React\Tests\EventLoop; + +class CallableStub +{ + public function __invoke() + { + } +} diff --git a/assets/php/vendor/react/event-loop/tests/ExtEvLoopTest.php b/assets/php/vendor/react/event-loop/tests/ExtEvLoopTest.php new file mode 100644 index 0000000..ab41c9f --- /dev/null +++ b/assets/php/vendor/react/event-loop/tests/ExtEvLoopTest.php @@ -0,0 +1,17 @@ +<?php + +namespace React\Tests\EventLoop; + +use React\EventLoop\ExtEvLoop; + +class ExtEvLoopTest extends AbstractLoopTest +{ + public function createLoop() + { + if (!class_exists('EvLoop')) { + $this->markTestSkipped('ExtEvLoop tests skipped because ext-ev extension is not installed.'); + } + + return new ExtEvLoop(); + } +} diff --git a/assets/php/vendor/react/event-loop/tests/ExtEventLoopTest.php b/assets/php/vendor/react/event-loop/tests/ExtEventLoopTest.php new file mode 100644 index 0000000..2f88d18 --- /dev/null +++ b/assets/php/vendor/react/event-loop/tests/ExtEventLoopTest.php @@ -0,0 +1,84 @@ +<?php + +namespace React\Tests\EventLoop; + +use React\EventLoop\ExtEventLoop; + +class ExtEventLoopTest extends AbstractLoopTest +{ + public function createLoop($readStreamCompatible = false) + { + if ('Linux' === PHP_OS && !extension_loaded('posix')) { + $this->markTestSkipped('libevent tests skipped on linux due to linux epoll issues.'); + } + + if (!extension_loaded('event')) { + $this->markTestSkipped('ext-event tests skipped because ext-event is not installed.'); + } + + return new ExtEventLoop(); + } + + public function createStream() + { + // Use a FIFO on linux to get around lack of support for disk-based file + // descriptors when using the EPOLL back-end. + if ('Linux' === PHP_OS) { + $this->fifoPath = tempnam(sys_get_temp_dir(), 'react-'); + + unlink($this->fifoPath); + + posix_mkfifo($this->fifoPath, 0600); + + $stream = fopen($this->fifoPath, 'r+'); + + // ext-event (as of 1.8.1) does not yet support in-memory temporary + // streams. Setting maxmemory:0 and performing a write forces PHP to + // back this temporary stream with a real file. + // + // This problem is mentioned at https://bugs.php.net/bug.php?id=64652&edit=3 + // but remains unresolved (despite that issue being closed). + } else { + $stream = fopen('php://temp/maxmemory:0', 'r+'); + + fwrite($stream, 'x'); + ftruncate($stream, 0); + } + + return $stream; + } + + public function writeToStream($stream, $content) + { + if ('Linux' !== PHP_OS) { + return parent::writeToStream($stream, $content); + } + + fwrite($stream, $content); + } + + /** + * @group epoll-readable-error + */ + public function testCanUseReadableStreamWithFeatureFds() + { + if (PHP_VERSION_ID > 70000) { + $this->markTestSkipped('Memory stream not supported'); + } + + $this->loop = $this->createLoop(true); + + $input = fopen('php://temp/maxmemory:0', 'r+'); + + fwrite($input, 'x'); + ftruncate($input, 0); + + $this->loop->addReadStream($input, $this->expectCallableExactly(2)); + + fwrite($input, "foo\n"); + $this->tickLoop($this->loop); + + fwrite($input, "bar\n"); + $this->tickLoop($this->loop); + } +} diff --git a/assets/php/vendor/react/event-loop/tests/ExtLibevLoopTest.php b/assets/php/vendor/react/event-loop/tests/ExtLibevLoopTest.php new file mode 100644 index 0000000..19a5e87 --- /dev/null +++ b/assets/php/vendor/react/event-loop/tests/ExtLibevLoopTest.php @@ -0,0 +1,22 @@ +<?php + +namespace React\Tests\EventLoop; + +use React\EventLoop\ExtLibevLoop; + +class ExtLibevLoopTest extends AbstractLoopTest +{ + public function createLoop() + { + if (!class_exists('libev\EventLoop')) { + $this->markTestSkipped('libev tests skipped because ext-libev is not installed.'); + } + + return new ExtLibevLoop(); + } + + public function testLibEvConstructor() + { + $loop = new ExtLibevLoop(); + } +} diff --git a/assets/php/vendor/react/event-loop/tests/ExtLibeventLoopTest.php b/assets/php/vendor/react/event-loop/tests/ExtLibeventLoopTest.php new file mode 100644 index 0000000..8497065 --- /dev/null +++ b/assets/php/vendor/react/event-loop/tests/ExtLibeventLoopTest.php @@ -0,0 +1,58 @@ +<?php + +namespace React\Tests\EventLoop; + +use React\EventLoop\ExtLibeventLoop; + +class ExtLibeventLoopTest extends AbstractLoopTest +{ + private $fifoPath; + + public function createLoop() + { + if ('Linux' === PHP_OS && !extension_loaded('posix')) { + $this->markTestSkipped('libevent tests skipped on linux due to linux epoll issues.'); + } + + if (!function_exists('event_base_new')) { + $this->markTestSkipped('libevent tests skipped because ext-libevent is not installed.'); + } + + return new ExtLibeventLoop(); + } + + public function tearDown() + { + if (file_exists($this->fifoPath)) { + unlink($this->fifoPath); + } + } + + public function createStream() + { + if ('Linux' !== PHP_OS) { + return parent::createStream(); + } + + $this->fifoPath = tempnam(sys_get_temp_dir(), 'react-'); + + unlink($this->fifoPath); + + // Use a FIFO on linux to get around lack of support for disk-based file + // descriptors when using the EPOLL back-end. + posix_mkfifo($this->fifoPath, 0600); + + $stream = fopen($this->fifoPath, 'r+'); + + return $stream; + } + + public function writeToStream($stream, $content) + { + if ('Linux' !== PHP_OS) { + return parent::writeToStream($stream, $content); + } + + fwrite($stream, $content); + } +} diff --git a/assets/php/vendor/react/event-loop/tests/SignalsHandlerTest.php b/assets/php/vendor/react/event-loop/tests/SignalsHandlerTest.php new file mode 100644 index 0000000..f8b7df3 --- /dev/null +++ b/assets/php/vendor/react/event-loop/tests/SignalsHandlerTest.php @@ -0,0 +1,55 @@ +<?php + +namespace React\Tests\EventLoop; + +use React\EventLoop\SignalsHandler; + +final class SignalsHandlerTest extends TestCase +{ + public function testEmittedEventsAndCallHandling() + { + $callCount = 0; + $func = function () use (&$callCount) { + $callCount++; + }; + $signals = new SignalsHandler(); + + $this->assertSame(0, $callCount); + + $signals->add(SIGUSR1, $func); + $this->assertSame(0, $callCount); + + $signals->add(SIGUSR1, $func); + $this->assertSame(0, $callCount); + + $signals->add(SIGUSR1, $func); + $this->assertSame(0, $callCount); + + $signals->call(SIGUSR1); + $this->assertSame(1, $callCount); + + $signals->add(SIGUSR2, $func); + $this->assertSame(1, $callCount); + + $signals->add(SIGUSR2, $func); + $this->assertSame(1, $callCount); + + $signals->call(SIGUSR2); + $this->assertSame(2, $callCount); + + $signals->remove(SIGUSR2, $func); + $this->assertSame(2, $callCount); + + $signals->remove(SIGUSR2, $func); + $this->assertSame(2, $callCount); + + $signals->call(SIGUSR2); + $this->assertSame(2, $callCount); + + $signals->remove(SIGUSR1, $func); + $this->assertSame(2, $callCount); + + $signals->call(SIGUSR1); + $this->assertSame(2, $callCount); + } +} diff --git a/assets/php/vendor/react/event-loop/tests/StreamSelectLoopTest.php b/assets/php/vendor/react/event-loop/tests/StreamSelectLoopTest.php new file mode 100644 index 0000000..bd19e1c --- /dev/null +++ b/assets/php/vendor/react/event-loop/tests/StreamSelectLoopTest.php @@ -0,0 +1,148 @@ +<?php + +namespace React\Tests\EventLoop; + +use React\EventLoop\LoopInterface; +use React\EventLoop\StreamSelectLoop; + +class StreamSelectLoopTest extends AbstractLoopTest +{ + protected function tearDown() + { + parent::tearDown(); + if (strncmp($this->getName(false), 'testSignal', 10) === 0 && extension_loaded('pcntl')) { + $this->resetSignalHandlers(); + } + } + + public function createLoop() + { + return new StreamSelectLoop(); + } + + public function testStreamSelectTimeoutEmulation() + { + $this->loop->addTimer( + 0.05, + $this->expectCallableOnce() + ); + + $start = microtime(true); + + $this->loop->run(); + + $end = microtime(true); + $interval = $end - $start; + + $this->assertGreaterThan(0.04, $interval); + } + + public function signalProvider() + { + return array( + array('SIGUSR1'), + array('SIGHUP'), + array('SIGTERM'), + ); + } + + /** + * Test signal interrupt when no stream is attached to the loop + * @dataProvider signalProvider + */ + public function testSignalInterruptNoStream($signal) + { + if (!extension_loaded('pcntl')) { + $this->markTestSkipped('"pcntl" extension is required to run this test.'); + } + + // dispatch signal handler every 10ms for 0.1s + $check = $this->loop->addPeriodicTimer(0.01, function() { + pcntl_signal_dispatch(); + }); + $loop = $this->loop; + $loop->addTimer(0.1, function () use ($check, $loop) { + $loop->cancelTimer($check); + }); + + $handled = false; + $this->assertTrue(pcntl_signal(constant($signal), function () use (&$handled) { + $handled = true; + })); + + // spawn external process to send signal to current process id + $this->forkSendSignal($signal); + + $this->loop->run(); + $this->assertTrue($handled); + } + + /** + * Test signal interrupt when a stream is attached to the loop + * @dataProvider signalProvider + */ + public function testSignalInterruptWithStream($signal) + { + if (!extension_loaded('pcntl')) { + $this->markTestSkipped('"pcntl" extension is required to run this test.'); + } + + // dispatch signal handler every 10ms + $this->loop->addPeriodicTimer(0.01, function() { + pcntl_signal_dispatch(); + }); + + // add stream to the loop + $loop = $this->loop; + list($writeStream, $readStream) = $this->createSocketPair(); + $loop->addReadStream($readStream, function ($stream) use ($loop) { + /** @var $loop LoopInterface */ + $read = fgets($stream); + if ($read === "end loop\n") { + $loop->stop(); + } + }); + $this->loop->addTimer(0.1, function() use ($writeStream) { + fwrite($writeStream, "end loop\n"); + }); + + $handled = false; + $this->assertTrue(pcntl_signal(constant($signal), function () use (&$handled) { + $handled = true; + })); + + // spawn external process to send signal to current process id + $this->forkSendSignal($signal); + + $this->loop->run(); + + $this->assertTrue($handled); + } + + /** + * reset all signal handlers to default + */ + protected function resetSignalHandlers() + { + foreach($this->signalProvider() as $signal) { + pcntl_signal(constant($signal[0]), SIG_DFL); + } + } + + /** + * fork child process to send signal to current process id + */ + protected function forkSendSignal($signal) + { + $currentPid = posix_getpid(); + $childPid = pcntl_fork(); + if ($childPid == -1) { + $this->fail("Failed to fork child process!"); + } else if ($childPid === 0) { + // this is executed in the child process + usleep(20000); + posix_kill($currentPid, constant($signal)); + die(); + } + } +} diff --git a/assets/php/vendor/react/event-loop/tests/TestCase.php b/assets/php/vendor/react/event-loop/tests/TestCase.php new file mode 100644 index 0000000..dbdd54c --- /dev/null +++ b/assets/php/vendor/react/event-loop/tests/TestCase.php @@ -0,0 +1,53 @@ +<?php + +namespace React\Tests\EventLoop; + +use PHPUnit\Framework\TestCase as BaseTestCase; +use React\EventLoop\LoopInterface; + +class TestCase extends BaseTestCase +{ + protected function expectCallableExactly($amount) + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->exactly($amount)) + ->method('__invoke'); + + return $mock; + } + + protected function expectCallableOnce() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke'); + + return $mock; + } + + protected function expectCallableNever() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->never()) + ->method('__invoke'); + + return $mock; + } + + protected function createCallableMock() + { + return $this->getMockBuilder('React\Tests\EventLoop\CallableStub')->getMock(); + } + + protected function tickLoop(LoopInterface $loop) + { + $loop->futureTick(function () use ($loop) { + $loop->stop(); + }); + + $loop->run(); + } +} diff --git a/assets/php/vendor/react/event-loop/tests/Timer/AbstractTimerTest.php b/assets/php/vendor/react/event-loop/tests/Timer/AbstractTimerTest.php new file mode 100644 index 0000000..294e683 --- /dev/null +++ b/assets/php/vendor/react/event-loop/tests/Timer/AbstractTimerTest.php @@ -0,0 +1,122 @@ +<?php + +namespace React\Tests\EventLoop\Timer; + +use React\EventLoop\LoopInterface; +use React\Tests\EventLoop\TestCase; + +abstract class AbstractTimerTest extends TestCase +{ + /** + * @return LoopInterface + */ + abstract public function createLoop(); + + public function testAddTimerReturnsNonPeriodicTimerInstance() + { + $loop = $this->createLoop(); + + $timer = $loop->addTimer(0.001, $this->expectCallableNever()); + + $this->assertInstanceOf('React\EventLoop\TimerInterface', $timer); + $this->assertFalse($timer->isPeriodic()); + } + + public function testAddTimerWillBeInvokedOnceAndBlocksLoopWhenRunning() + { + $loop = $this->createLoop(); + + $loop->addTimer(0.001, $this->expectCallableOnce()); + + $start = microtime(true); + $loop->run(); + $end = microtime(true); + + // make no strict assumptions about actual time interval. + // must be at least 0.001s (1ms) and should not take longer than 0.1s + $this->assertGreaterThanOrEqual(0.001, $end - $start); + $this->assertLessThan(0.1, $end - $start); + } + + public function testAddPeriodicTimerReturnsPeriodicTimerInstance() + { + $loop = $this->createLoop(); + + $periodic = $loop->addPeriodicTimer(0.1, $this->expectCallableNever()); + + $this->assertInstanceOf('React\EventLoop\TimerInterface', $periodic); + $this->assertTrue($periodic->isPeriodic()); + } + + public function testAddPeriodicTimerWillBeInvokedUntilItIsCancelled() + { + $loop = $this->createLoop(); + + $periodic = $loop->addPeriodicTimer(0.1, $this->expectCallableExactly(3)); + + // make no strict assumptions about actual time interval. + // leave some room to ensure this ticks exactly 3 times. + $loop->addTimer(0.399, function () use ($loop, $periodic) { + $loop->cancelTimer($periodic); + }); + + $loop->run(); + } + + public function testAddPeriodicTimerWillBeInvokedWithMaximumAccuracyUntilItIsCancelled() + { + $loop = $this->createLoop(); + + $i = 0; + $periodic = $loop->addPeriodicTimer(0.001, function () use (&$i) { + ++$i; + }); + + $loop->addTimer(0.02, function () use ($loop, $periodic) { + $loop->cancelTimer($periodic); + }); + + $loop->run(); + + // make no strict assumptions about number of invocations. + // we know it must be no more than 20 times and should at least be + // invoked twice for really slow loops + $this->assertLessThanOrEqual(20, $i); + $this->assertGreaterThan(2, $i); + } + + public function testAddPeriodicTimerCancelsItself() + { + $loop = $this->createLoop(); + + $i = 0; + $loop->addPeriodicTimer(0.001, function ($timer) use (&$i, $loop) { + $i++; + + if ($i === 5) { + $loop->cancelTimer($timer); + } + }); + + $start = microtime(true); + $loop->run(); + $end = microtime(true); + + $this->assertEquals(5, $i); + + // make no strict assumptions about time interval. + // 5 invocations must take at least 0.005s (5ms) and should not take + // longer than 0.1s for slower loops. + $this->assertGreaterThanOrEqual(0.005, $end - $start); + $this->assertLessThan(0.1, $end - $start); + } + + public function testMinimumIntervalOneMicrosecond() + { + $loop = $this->createLoop(); + + $timer = $loop->addTimer(0, function () {}); + + $this->assertEquals(0.000001, $timer->getInterval()); + } +} diff --git a/assets/php/vendor/react/event-loop/tests/Timer/ExtEvTimerTest.php b/assets/php/vendor/react/event-loop/tests/Timer/ExtEvTimerTest.php new file mode 100644 index 0000000..bfa9186 --- /dev/null +++ b/assets/php/vendor/react/event-loop/tests/Timer/ExtEvTimerTest.php @@ -0,0 +1,17 @@ +<?php + +namespace React\Tests\EventLoop\Timer; + +use React\EventLoop\ExtEvLoop; + +class ExtEvTimerTest extends AbstractTimerTest +{ + public function createLoop() + { + if (!class_exists('EvLoop')) { + $this->markTestSkipped('ExtEvLoop tests skipped because ext-ev extension is not installed.'); + } + + return new ExtEvLoop(); + } +} diff --git a/assets/php/vendor/react/event-loop/tests/Timer/ExtEventTimerTest.php b/assets/php/vendor/react/event-loop/tests/Timer/ExtEventTimerTest.php new file mode 100644 index 0000000..a7a6d00 --- /dev/null +++ b/assets/php/vendor/react/event-loop/tests/Timer/ExtEventTimerTest.php @@ -0,0 +1,17 @@ +<?php + +namespace React\Tests\EventLoop\Timer; + +use React\EventLoop\ExtEventLoop; + +class ExtEventTimerTest extends AbstractTimerTest +{ + public function createLoop() + { + if (!extension_loaded('event')) { + $this->markTestSkipped('ext-event tests skipped because ext-event is not installed.'); + } + + return new ExtEventLoop(); + } +} diff --git a/assets/php/vendor/react/event-loop/tests/Timer/ExtLibevTimerTest.php b/assets/php/vendor/react/event-loop/tests/Timer/ExtLibevTimerTest.php new file mode 100644 index 0000000..65e82be --- /dev/null +++ b/assets/php/vendor/react/event-loop/tests/Timer/ExtLibevTimerTest.php @@ -0,0 +1,17 @@ +<?php + +namespace React\Tests\EventLoop\Timer; + +use React\EventLoop\ExtLibevLoop; + +class ExtLibevTimerTest extends AbstractTimerTest +{ + public function createLoop() + { + if (!class_exists('libev\EventLoop')) { + $this->markTestSkipped('libev tests skipped because ext-libev is not installed.'); + } + + return new ExtLibevLoop(); + } +} diff --git a/assets/php/vendor/react/event-loop/tests/Timer/ExtLibeventTimerTest.php b/assets/php/vendor/react/event-loop/tests/Timer/ExtLibeventTimerTest.php new file mode 100644 index 0000000..9089b9a --- /dev/null +++ b/assets/php/vendor/react/event-loop/tests/Timer/ExtLibeventTimerTest.php @@ -0,0 +1,17 @@ +<?php + +namespace React\Tests\EventLoop\Timer; + +use React\EventLoop\ExtLibeventLoop; + +class ExtLibeventTimerTest extends AbstractTimerTest +{ + public function createLoop() + { + if (!function_exists('event_base_new')) { + $this->markTestSkipped('libevent tests skipped because ext-libevent is not installed.'); + } + + return new ExtLibeventLoop(); + } +} diff --git a/assets/php/vendor/react/event-loop/tests/Timer/StreamSelectTimerTest.php b/assets/php/vendor/react/event-loop/tests/Timer/StreamSelectTimerTest.php new file mode 100644 index 0000000..cfe1d7d --- /dev/null +++ b/assets/php/vendor/react/event-loop/tests/Timer/StreamSelectTimerTest.php @@ -0,0 +1,13 @@ +<?php + +namespace React\Tests\EventLoop\Timer; + +use React\EventLoop\StreamSelectLoop; + +class StreamSelectTimerTest extends AbstractTimerTest +{ + public function createLoop() + { + return new StreamSelectLoop(); + } +} diff --git a/assets/php/vendor/react/event-loop/tests/Timer/TimersTest.php b/assets/php/vendor/react/event-loop/tests/Timer/TimersTest.php new file mode 100644 index 0000000..b279478 --- /dev/null +++ b/assets/php/vendor/react/event-loop/tests/Timer/TimersTest.php @@ -0,0 +1,27 @@ +<?php + +namespace React\Tests\EventLoop\Timer; + +use React\Tests\EventLoop\TestCase; +use React\EventLoop\Timer\Timer; +use React\EventLoop\Timer\Timers; + +class TimersTest extends TestCase +{ + public function testBlockedTimer() + { + $timers = new Timers(); + $timers->tick(); + + // simulate a bunch of processing on stream events, + // part of which schedules a future timer... + sleep(1); + $timers->add(new Timer(0.5, function () { + $this->fail("Timer shouldn't be called"); + })); + + $timers->tick(); + + $this->assertTrue(true); + } +} diff --git a/assets/php/vendor/react/event-loop/tests/bootstrap.php b/assets/php/vendor/react/event-loop/tests/bootstrap.php new file mode 100644 index 0000000..ea7dd4c --- /dev/null +++ b/assets/php/vendor/react/event-loop/tests/bootstrap.php @@ -0,0 +1,15 @@ +<?php + +$loader = @include __DIR__ . '/../vendor/autoload.php'; +if (!$loader) { + $loader = require __DIR__ . '/../../../../vendor/autoload.php'; +} +$loader->addPsr4('React\\Tests\\EventLoop\\', __DIR__); + +if (!defined('SIGUSR1')) { + define('SIGUSR1', 1); +} + +if (!defined('SIGUSR2')) { + define('SIGUSR2', 2); +} diff --git a/assets/php/vendor/react/event-loop/travis-init.sh b/assets/php/vendor/react/event-loop/travis-init.sh new file mode 100755 index 0000000..29ce884 --- /dev/null +++ b/assets/php/vendor/react/event-loop/travis-init.sh @@ -0,0 +1,42 @@ +#!/bin/bash +set -e +set -o pipefail + +if [[ "$TRAVIS_PHP_VERSION" != "hhvm" && + "$TRAVIS_PHP_VERSION" != "hhvm-nightly" ]]; then + + # install 'event' and 'ev' PHP extension + if [[ "$TRAVIS_PHP_VERSION" != "5.3" ]]; then + echo "yes" | pecl install event + echo "yes" | pecl install ev + fi + + # install 'libevent' PHP extension (does not support php 7) + if [[ "$TRAVIS_PHP_VERSION" != "7.0" && + "$TRAVIS_PHP_VERSION" != "7.1" && + "$TRAVIS_PHP_VERSION" != "7.2" ]]; then + curl http://pecl.php.net/get/libevent-0.1.0.tgz | tar -xz + pushd libevent-0.1.0 + phpize + ./configure + make + make install + popd + echo "extension=libevent.so" >> "$(php -r 'echo php_ini_loaded_file();')" + fi + + # install 'libev' PHP extension (does not support php 7) + if [[ "$TRAVIS_PHP_VERSION" != "7.0" && + "$TRAVIS_PHP_VERSION" != "7.1" && + "$TRAVIS_PHP_VERSION" != "7.2" ]]; then + git clone --recursive https://github.com/m4rw3r/php-libev + pushd php-libev + phpize + ./configure --with-libev + make + make install + popd + echo "extension=libev.so" >> "$(php -r 'echo php_ini_loaded_file();')" + fi + +fi diff --git a/assets/php/vendor/react/promise-timer/.gitignore b/assets/php/vendor/react/promise-timer/.gitignore new file mode 100644 index 0000000..de4a392 --- /dev/null +++ b/assets/php/vendor/react/promise-timer/.gitignore @@ -0,0 +1,2 @@ +/vendor +/composer.lock diff --git a/assets/php/vendor/react/promise-timer/.travis.yml b/assets/php/vendor/react/promise-timer/.travis.yml new file mode 100644 index 0000000..a71864a --- /dev/null +++ b/assets/php/vendor/react/promise-timer/.travis.yml @@ -0,0 +1,26 @@ +language: php + +php: +# - 5.3 # requires old distro, see below + - 5.4 + - 5.5 + - 5.6 + - 7.0 + - 7.1 + - hhvm # ignore errors, see below + +# lock distro so new future defaults will not break the build +dist: trusty + +matrix: + include: + - php: 5.3 + dist: precise + allow_failures: + - php: hhvm + +install: + - composer install --no-interaction + +script: + - vendor/bin/phpunit --coverage-text diff --git a/assets/php/vendor/react/promise-timer/CHANGELOG.md b/assets/php/vendor/react/promise-timer/CHANGELOG.md new file mode 100644 index 0000000..0a21244 --- /dev/null +++ b/assets/php/vendor/react/promise-timer/CHANGELOG.md @@ -0,0 +1,40 @@ +# Changelog + +## 1.2.1 (2017-12-22) + +* README improvements + (#28 by @jsor) + +* Improve test suite by adding forward compatiblity with PHPUnit 6 and + fix test suite forward compatibility with upcoming EventLoop releases + (#30 and #31 by @clue) + +## 1.2.0 (2017-08-08) + +* Feature: Only start timers if input Promise is still pending and + return a settled output promise if the input is already settled. + (#25 by @clue) + +* Feature: Cap minimum timer interval at 1µs across all versions + (#23 by @clue) + +* Feature: Forward compatibility with EventLoop v1.0 and v0.5 + (#27 by @clue) + +* Improve test suite by adding PHPUnit to require-dev and + lock Travis distro so new defaults will not break the build + (#24 and #26 by @clue) + +## 1.1.1 (2016-12-27) + +* Improve test suite to use PSR-4 autoloader and proper namespaces. + (#21 by @clue) + +## 1.1.0 (2016-02-29) + +* Feature: Support promise cancellation for all timer primitives + (#18 by @clue) + +## 1.0.0 (2015-09-29) + +* First tagged release diff --git a/assets/php/vendor/react/promise-timer/LICENSE b/assets/php/vendor/react/promise-timer/LICENSE new file mode 100644 index 0000000..dc09d1e --- /dev/null +++ b/assets/php/vendor/react/promise-timer/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Christian Lück + +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/assets/php/vendor/react/promise-timer/README.md b/assets/php/vendor/react/promise-timer/README.md new file mode 100644 index 0000000..2ea94fa --- /dev/null +++ b/assets/php/vendor/react/promise-timer/README.md @@ -0,0 +1,372 @@ +# PromiseTimer + +[](https://travis-ci.org/reactphp/promise-timer) + +A trivial implementation of timeouts for `Promise`s, built on top of [ReactPHP](https://reactphp.org/). + +**Table of contents** + +* [Usage](#usage) + * [timeout()](#timeout) + * [Timeout cancellation](#timeout-cancellation) + * [Cancellation handler](#cancellation-handler) + * [Input cancellation](#input-cancellation) + * [Output cancellation](#output-cancellation) + * [Collections](#collections) + * [resolve()](#resolve) + * [Resolve cancellation](#resolve-cancellation) + * [reject()](#reject) + * [Reject cancellation](#reject-cancellation) + * [TimeoutException](#timeoutexception) +* [Install](#install) +* [Tests](#tests) +* [License](#license) + +## Usage + +This lightweight library consists only of a few simple functions. +All functions reside under the `React\Promise\Timer` namespace. + +The below examples assume you use an import statement similar to this: + +```php +use React\Promise\Timer; + +Timer\timeout(…); +``` + +Alternatively, you can also refer to them with their fully-qualified name: + +```php +\React\Promise\Timer\timeout(…); +``` + +### timeout() + +The `timeout(PromiseInterface $promise, $time, LoopInterface $loop)` function +can be used to *cancel* operations that take *too long*. +You need to pass in an input `$promise` that represents a pending operation and timeout parameters. +It returns a new `Promise` with the following resolution behavior: + +* If the input `$promise` resolves before `$time` seconds, resolve the resulting promise with its fulfillment value. +* If the input `$promise` rejects before `$time` seconds, reject the resulting promise with its rejection value. +* If the input `$promise` does not settle before `$time` seconds, *cancel* the operation and reject the resulting promise with a [`TimeoutException`](#timeoutexception). + +Internally, the given `$time` value will be used to start a timer that will +*cancel* the pending operation once it triggers. +This implies that if you pass a really small (or negative) value, it will still +start a timer and will thus trigger at the earliest possible time in the future. + +If the input `$promise` is already settled, then the resulting promise will +resolve or reject immediately without starting a timer at all. + +A common use case for handling only resolved values looks like this: + +```php +$promise = accessSomeRemoteResource(); +Timer\timeout($promise, 10.0, $loop)->then(function ($value) { + // the operation finished within 10.0 seconds +}); +``` + +A more complete example could look like this: + +```php +$promise = accessSomeRemoteResource(); +Timer\timeout($promise, 10.0, $loop)->then( + function ($value) { + // the operation finished within 10.0 seconds + }, + function ($error) { + if ($error instanceof Timer\TimeoutException) { + // the operation has failed due to a timeout + } else { + // the input operation has failed due to some other error + } + } +); +``` + +Or if you're using [react/promise v2.2.0](https://github.com/reactphp/promise) or up: + +```php +Timer\timeout($promise, 10.0, $loop) + ->then(function ($value) { + // the operation finished within 10.0 seconds + }) + ->otherwise(function (Timer\TimeoutException $error) { + // the operation has failed due to a timeout + }) + ->otherwise(function ($error) { + // the input operation has failed due to some other error + }) +; +``` + +#### Timeout cancellation + +As discussed above, the [`timeout()`](#timeout) function will *cancel* the +underlying operation if it takes *too long*. +This means that you can be sure the resulting promise will then be rejected +with a [`TimeoutException`](#timeoutexception). + +However, what happens to the underlying input `$promise` is a bit more tricky: +Once the timer fires, we will try to call +[`$promise->cancel()`](https://github.com/reactphp/promise#cancellablepromiseinterfacecancel) +on the input `$promise` which in turn invokes its [cancellation handler](#cancellation-handler). + +This means that it's actually up the input `$promise` to handle +[cancellation support](https://github.com/reactphp/promise#cancellablepromiseinterface). + +* A common use case involves cleaning up any resources like open network sockets or + file handles or terminating external processes or timers. + +* If the given input `$promise` does not support cancellation, then this is a NO-OP. + This means that while the resulting promise will still be rejected, the underlying + input `$promise` may still be pending and can hence continue consuming resources. + +See the following chapter for more details on the cancellation handler. + +#### Cancellation handler + +For example, an implementation for the above operation could look like this: + +```php +function accessSomeRemoteResource() +{ + return new Promise( + function ($resolve, $reject) use (&$socket) { + // this will be called once the promise is created + // a common use case involves opening any resources and eventually resolving + $socket = createSocket(); + $socket->on('data', function ($data) use ($resolve) { + $resolve($data); + }); + }, + function ($resolve, $reject) use (&$socket) { + // this will be called once calling `cancel()` on this promise + // a common use case involves cleaning any resources and then rejecting + $socket->close(); + $reject(new \RuntimeException('Operation cancelled')); + } + ); +} +``` + +In this example, calling `$promise->cancel()` will invoke the registered cancellation +handler which then closes the network socket and rejects the `Promise` instance. + +If no cancellation handler is passed to the `Promise` constructor, then invoking +its `cancel()` method it is effectively a NO-OP. +This means that it may still be pending and can hence continue consuming resources. + +For more details on the promise cancellation, please refer to the +[Promise documentation](https://github.com/reactphp/promise#cancellablepromiseinterface). + +#### Input cancellation + +Irrespective of the timout handling, you can also explicitly `cancel()` the +input `$promise` at any time. +This means that the `timeout()` handling does not affect cancellation of the +input `$promise`, as demonstrated in the following example: + +```php +$promise = accessSomeRemoteResource(); +$timeout = Timer\timeout($promise, 10.0, $loop); + +$promise->cancel(); +``` + +The registered [cancellation handler](#cancellation-handler) is responsible for +handling the `cancel()` call: + +* A described above, a common use involves resource cleanup and will then *reject* + the `Promise`. + If the input `$promise` is being rejected, then the timeout will be aborted + and the resulting promise will also be rejected. +* If the input `$promise` is still pending, then the timout will continue + running until the timer expires. + The same happens if the input `$promise` does not register a + [cancellation handler](#cancellation-handler). + +#### Output cancellation + +Similarily, you can also explicitly `cancel()` the resulting promise like this: + +```php +$promise = accessSomeRemoteResource(); +$timeout = Timer\timeout($promise, 10.0, $loop); + +$timeout->cancel(); +``` + +Note how this looks very similar to the above [input cancellation](#input-cancellation) +example. Accordingly, it also behaves very similar. + +Calling `cancel()` on the resulting promise will merely try +to `cancel()` the input `$promise`. +This means that we do not take over responsibility of the outcome and it's +entirely up to the input `$promise` to handle cancellation support. + +The registered [cancellation handler](#cancellation-handler) is responsible for +handling the `cancel()` call: + +* As described above, a common use involves resource cleanup and will then *reject* + the `Promise`. + If the input `$promise` is being rejected, then the timeout will be aborted + and the resulting promise will also be rejected. +* If the input `$promise` is still pending, then the timout will continue + running until the timer expires. + The same happens if the input `$promise` does not register a + [cancellation handler](#cancellation-handler). + +To re-iterate, note that calling `cancel()` on the resulting promise will merely +try to cancel the input `$promise` only. +It is then up to the cancellation handler of the input promise to settle the promise. +If the input promise is still pending when the timeout occurs, then the normal +[timeout cancellation](#timeout-cancellation) handling will trigger, effectively rejecting +the output promise with a [`TimeoutException`](#timeoutexception). + +This is done for consistency with the [timeout cancellation](#timeout-cancellation) +handling and also because it is assumed this is often used like this: + +```php +$timeout = Timer\timeout(accessSomeRemoteResource(), 10.0, $loop); + +$timeout->cancel(); +``` + +As described above, this example works as expected and cleans up any resources +allocated for the input `$promise`. + +Note that if the given input `$promise` does not support cancellation, then this +is a NO-OP. +This means that while the resulting promise will still be rejected after the +timeout, the underlying input `$promise` may still be pending and can hence +continue consuming resources. + +#### Collections + +If you want to wait for multiple promises to resolve, you can use the normal promise primitives like this: + +```php +$promises = array( + accessSomeRemoteResource(), + accessSomeRemoteResource(), + accessSomeRemoteResource() +); + +$promise = \React\Promise\all($promises); + +Timer\timeout($promise, 10, $loop)->then(function ($values) { + // *all* promises resolved +}); +``` + +The applies to all promise collection primitives alike, i.e. `all()`, `race()`, `any()`, `some()` etc. + +For more details on the promise primitives, please refer to the +[Promise documentation](https://github.com/reactphp/promise#functions). + +### resolve() + +The `resolve($time, LoopInterface $loop)` function can be used to create a new Promise that +resolves in `$time` seconds with the `$time` as the fulfillment value. + +```php +Timer\resolve(1.5, $loop)->then(function ($time) { + echo 'Thanks for waiting ' . $time . ' seconds' . PHP_EOL; +}); +``` + +Internally, the given `$time` value will be used to start a timer that will +resolve the promise once it triggers. +This implies that if you pass a really small (or negative) value, it will still +start a timer and will thus trigger at the earliest possible time in the future. + +#### Resolve cancellation + +You can explicitly `cancel()` the resulting timer promise at any time: + +```php +$timer = Timer\resolve(2.0, $loop); + +$timer->cancel(); +``` + +This will abort the timer and *reject* with a `RuntimeException`. + +### reject() + +The `reject($time, LoopInterface $loop)` function can be used to create a new Promise +which rejects in `$time` seconds with a `TimeoutException`. + +```php +Timer\reject(2.0, $loop)->then(null, function (TimeoutException $e) { + echo 'Rejected after ' . $e->getTimeout() . ' seconds ' . PHP_EOL; +}); +``` + +Internally, the given `$time` value will be used to start a timer that will +reject the promise once it triggers. +This implies that if you pass a really small (or negative) value, it will still +start a timer and will thus trigger at the earliest possible time in the future. + +This function complements the [`resolve()`](#resolve) function +and can be used as a basic building block for higher-level promise consumers. + +#### Reject cancellation + +You can explicitly `cancel()` the resulting timer promise at any time: + +```php +$timer = Timer\reject(2.0, $loop); + +$timer->cancel(); +``` + +This will abort the timer and *reject* with a `RuntimeException`. + +### TimeoutException + +The `TimeoutException` extends PHP's built-in `RuntimeException`. + +The `getTimeout()` method can be used to get the timeout value in seconds. + +## Install + +The recommended way to install this library is [through Composer](https://getcomposer.org). +[New to Composer?](https://getcomposer.org/doc/00-intro.md) + +This project follows [SemVer](http://semver.org/). +This will install the latest supported version: + +```bash +$ composer require react/promise-timer:^1.2.1 +``` + +See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. + +This project aims to run on any platform and thus does not require any PHP +extensions and supports running on legacy PHP 5.3 through current PHP 7+ and +HHVM. +It's *highly recommended to use PHP 7+* for this project. + +## Tests + +To run the test suite, you first need to clone this repo and then install all +dependencies [through Composer](https://getcomposer.org): + +```bash +$ composer install +``` + +To run the test suite, go to the project root and run: + +```bash +$ php vendor/bin/phpunit +``` + +## License + +MIT, see [LICENSE file](LICENSE). diff --git a/assets/php/vendor/react/promise-timer/composer.json b/assets/php/vendor/react/promise-timer/composer.json new file mode 100644 index 0000000..e425dc6 --- /dev/null +++ b/assets/php/vendor/react/promise-timer/composer.json @@ -0,0 +1,28 @@ +{ + "name": "react/promise-timer", + "description": "A trivial implementation of timeouts for Promises, built on top of ReactPHP.", + "keywords": ["Promise", "timeout", "timer", "event-loop", "ReactPHP", "async"], + "homepage": "https://github.com/react/promise-timer", + "license": "MIT", + "authors": [ + { + "name": "Christian Lück", + "email": "christian@lueck.tv" + } + ], + "autoload": { + "psr-4": { "React\\Promise\\Timer\\": "src/" }, + "files": [ "src/functions.php" ] + }, + "autoload-dev": { + "psr-4": { "React\\Tests\\Promise\\Timer\\": "tests/" } + }, + "require": { + "php": ">=5.3", + "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3.5", + "react/promise": "~2.1|~1.2" + }, + "require-dev": { + "phpunit/phpunit": "^6.4 || ^5.7 || ^4.8.35" + } +} diff --git a/assets/php/vendor/react/promise-timer/phpunit.xml.dist b/assets/php/vendor/react/promise-timer/phpunit.xml.dist new file mode 100644 index 0000000..bb79fba --- /dev/null +++ b/assets/php/vendor/react/promise-timer/phpunit.xml.dist @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<phpunit bootstrap="vendor/autoload.php" + colors="true" + convertErrorsToExceptions="true" + convertNoticesToExceptions="true" + convertWarningsToExceptions="true" +> + <testsuites> + <testsuite name="Promise Timer Test Suite"> + <directory>./tests/</directory> + </testsuite> + </testsuites> + <filter> + <whitelist> + <directory>./src/</directory> + </whitelist> + </filter> +</phpunit>
\ No newline at end of file diff --git a/assets/php/vendor/react/promise-timer/src/TimeoutException.php b/assets/php/vendor/react/promise-timer/src/TimeoutException.php new file mode 100644 index 0000000..18ea72f --- /dev/null +++ b/assets/php/vendor/react/promise-timer/src/TimeoutException.php @@ -0,0 +1,22 @@ +<?php + +namespace React\Promise\Timer; + +use RuntimeException; + +class TimeoutException extends RuntimeException +{ + private $timeout; + + public function __construct($timeout, $message = null, $code = null, $previous = null) + { + parent::__construct($message, $code, $previous); + + $this->timeout = $timeout; + } + + public function getTimeout() + { + return $this->timeout; + } +} diff --git a/assets/php/vendor/react/promise-timer/src/functions.php b/assets/php/vendor/react/promise-timer/src/functions.php new file mode 100644 index 0000000..6ad9867 --- /dev/null +++ b/assets/php/vendor/react/promise-timer/src/functions.php @@ -0,0 +1,70 @@ +<?php + +namespace React\Promise\Timer; + +use React\Promise\CancellablePromiseInterface; +use React\EventLoop\LoopInterface; +use React\Promise\PromiseInterface; +use React\Promise\Promise; + +function timeout(PromiseInterface $promise, $time, LoopInterface $loop) +{ + // cancelling this promise will only try to cancel the input promise, + // thus leaving responsibility to the input promise. + $canceller = null; + if ($promise instanceof CancellablePromiseInterface) { + $canceller = array($promise, 'cancel'); + } + + return new Promise(function ($resolve, $reject) use ($loop, $time, $promise) { + $timer = null; + $promise->then(function ($v) use (&$timer, $loop, $resolve) { + if ($timer) { + $loop->cancelTimer($timer); + } + $timer = false; + $resolve($v); + }, function ($v) use (&$timer, $loop, $reject) { + if ($timer) { + $loop->cancelTimer($timer); + } + $timer = false; + $reject($v); + }); + + // promise already resolved => no need to start timer + if ($timer === false) { + return; + } + + // start timeout timer which will cancel the input promise + $timer = $loop->addTimer($time, function () use ($time, $promise, $reject) { + $reject(new TimeoutException($time, 'Timed out after ' . $time . ' seconds')); + + if ($promise instanceof CancellablePromiseInterface) { + $promise->cancel(); + } + }); + }, $canceller); +} + +function resolve($time, LoopInterface $loop) +{ + return new Promise(function ($resolve) use ($loop, $time, &$timer) { + // resolve the promise when the timer fires in $time seconds + $timer = $loop->addTimer($time, function () use ($time, $resolve) { + $resolve($time); + }); + }, function ($resolveUnused, $reject) use (&$timer, $loop) { + // cancelling this promise will cancel the timer and reject + $loop->cancelTimer($timer); + $reject(new \RuntimeException('Timer cancelled')); + }); +} + +function reject($time, LoopInterface $loop) +{ + return resolve($time, $loop)->then(function ($time) { + throw new TimeoutException($time, 'Timer expired after ' . $time . ' seconds'); + }); +} diff --git a/assets/php/vendor/react/promise-timer/tests/CallableStub.php b/assets/php/vendor/react/promise-timer/tests/CallableStub.php new file mode 100644 index 0000000..a391aa5 --- /dev/null +++ b/assets/php/vendor/react/promise-timer/tests/CallableStub.php @@ -0,0 +1,10 @@ +<?php + +namespace React\Tests\Promise\Timer; + +class CallableStub +{ + public function __invoke() + { + } +} diff --git a/assets/php/vendor/react/promise-timer/tests/FunctionRejectTest.php b/assets/php/vendor/react/promise-timer/tests/FunctionRejectTest.php new file mode 100644 index 0000000..6153fcc --- /dev/null +++ b/assets/php/vendor/react/promise-timer/tests/FunctionRejectTest.php @@ -0,0 +1,49 @@ +<?php + +namespace React\Tests\Promise\Timer; + +use React\Promise\Timer; + +class FunctionRejectTest extends TestCase +{ + public function testPromiseIsPendingWithoutRunningLoop() + { + $promise = Timer\reject(0.01, $this->loop); + + $this->expectPromisePending($promise); + } + + public function testPromiseExpiredIsPendingWithoutRunningLoop() + { + $promise = Timer\reject(-1, $this->loop); + + $this->expectPromisePending($promise); + } + + public function testPromiseWillBeRejectedOnTimeout() + { + $promise = Timer\reject(0.01, $this->loop); + + $this->loop->run(); + + $this->expectPromiseRejected($promise); + } + + public function testPromiseExpiredWillBeRejectedOnTimeout() + { + $promise = Timer\reject(-1, $this->loop); + + $this->loop->run(); + + $this->expectPromiseRejected($promise); + } + + public function testCancelingPromiseWillRejectTimer() + { + $promise = Timer\reject(0.01, $this->loop); + + $promise->cancel(); + + $this->expectPromiseRejected($promise); + } +} diff --git a/assets/php/vendor/react/promise-timer/tests/FunctionResolveTest.php b/assets/php/vendor/react/promise-timer/tests/FunctionResolveTest.php new file mode 100644 index 0000000..0bfdc21 --- /dev/null +++ b/assets/php/vendor/react/promise-timer/tests/FunctionResolveTest.php @@ -0,0 +1,71 @@ +<?php + +namespace React\Tests\Promise\Timer; + +use React\Promise\Timer; + +class FunctionResolveTest extends TestCase +{ + public function testPromiseIsPendingWithoutRunningLoop() + { + $promise = Timer\resolve(0.01, $this->loop); + + $this->expectPromisePending($promise); + } + + public function testPromiseExpiredIsPendingWithoutRunningLoop() + { + $promise = Timer\resolve(-1, $this->loop); + + $this->expectPromisePending($promise); + } + + public function testPromiseWillBeResolvedOnTimeout() + { + $promise = Timer\resolve(0.01, $this->loop); + + $this->loop->run(); + + $this->expectPromiseResolved($promise); + } + + public function testPromiseExpiredWillBeResolvedOnTimeout() + { + $promise = Timer\resolve(-1, $this->loop); + + $this->loop->run(); + + $this->expectPromiseResolved($promise); + } + + public function testWillStartLoopTimer() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('addTimer')->with($this->equalTo(0.01)); + + Timer\resolve(0.01, $loop); + } + + public function testCancellingPromiseWillCancelLoopTimer() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $timer = $this->getMockBuilder(interface_exists('React\EventLoop\TimerInterface') ? 'React\EventLoop\TimerInterface' : 'React\EventLoop\Timer\TimerInterface')->getMock(); + $loop->expects($this->once())->method('addTimer')->will($this->returnValue($timer)); + + $promise = Timer\resolve(0.01, $loop); + + $loop->expects($this->once())->method('cancelTimer')->with($this->equalTo($timer)); + + $promise->cancel(); + } + + public function testCancelingPromiseWillRejectTimer() + { + $promise = Timer\resolve(0.01, $this->loop); + + $promise->cancel(); + + $this->expectPromiseRejected($promise); + } +} diff --git a/assets/php/vendor/react/promise-timer/tests/FunctionTimeoutTest.php b/assets/php/vendor/react/promise-timer/tests/FunctionTimeoutTest.php new file mode 100644 index 0000000..aaca2da --- /dev/null +++ b/assets/php/vendor/react/promise-timer/tests/FunctionTimeoutTest.php @@ -0,0 +1,169 @@ +<?php + +namespace React\Tests\Promise\Timer; + +use React\Promise\Timer; +use React\Promise; + +class FunctionTimerTest extends TestCase +{ + public function testResolvedWillResolveRightAway() + { + $promise = Promise\resolve(); + + $promise = Timer\timeout($promise, 3, $this->loop); + + $this->expectPromiseResolved($promise); + } + + public function testResolvedExpiredWillResolveRightAway() + { + $promise = Promise\resolve(); + + $promise = Timer\timeout($promise, -1, $this->loop); + + $this->expectPromiseResolved($promise); + } + + public function testResolvedWillNotStartTimer() + { + $promise = Promise\resolve(); + + Timer\timeout($promise, 3, $this->loop); + + $time = microtime(true); + $this->loop->run(); + $time = microtime(true) - $time; + + $this->assertLessThan(0.5, $time); + } + + public function testRejectedWillRejectRightAway() + { + $promise = Promise\reject(); + + $promise = Timer\timeout($promise, 3, $this->loop); + + $this->expectPromiseRejected($promise); + } + + public function testRejectedWillNotStartTimer() + { + $promise = Promise\reject(); + + Timer\timeout($promise, 3, $this->loop); + + $time = microtime(true); + $this->loop->run(); + $time = microtime(true) - $time; + + $this->assertLessThan(0.5, $time); + } + + public function testPendingWillRejectOnTimeout() + { + $promise = $this->getMockBuilder('React\Promise\PromiseInterface')->getMock(); + + $promise = Timer\timeout($promise, 0.01, $this->loop); + + $this->loop->run(); + + $this->expectPromiseRejected($promise); + } + + public function testPendingCancellableWillBeCancelledOnTimeout() + { + $promise = $this->getMockBuilder('React\Promise\CancellablePromiseInterface')->getMock(); + $promise->expects($this->once())->method('cancel'); + + Timer\timeout($promise, 0.01, $this->loop); + + $this->loop->run(); + } + + public function testCancelTimeoutWithoutCancellationhandlerWillNotCancelTimerAndWillNotReject() + { + $promise = new \React\Promise\Promise(function () { }); + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $timer = $this->getMockBuilder('React\EventLoop\Timer\TimerInterface')->getMock(); + $loop->expects($this->once())->method('addTimer')->will($this->returnValue($timer)); + $loop->expects($this->never())->method('cancelTimer'); + + $timeout = Timer\timeout($promise, 0.01, $loop); + + $timeout->cancel(); + + $this->expectPromisePending($timeout); + } + + public function testResolvedPromiseWillNotStartTimer() + { + $promise = new \React\Promise\Promise(function ($resolve) { $resolve(true); }); + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->never())->method('addTimer'); + + $timeout = Timer\timeout($promise, 0.01, $loop); + + $this->expectPromiseResolved($timeout); + } + + public function testRejectedPromiseWillNotStartTimer() + { + $promise = Promise\reject(new \RuntimeException()); + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->never())->method('addTimer'); + + $timeout = Timer\timeout($promise, 0.01, $loop); + + $this->expectPromiseRejected($timeout); + } + + public function testCancelTimeoutWillCancelGivenPromise() + { + $promise = new \React\Promise\Promise(function () { }, $this->expectCallableOnce()); + + $timeout = Timer\timeout($promise, 0.01, $this->loop); + + $timeout->cancel(); + } + + public function testCancelGivenPromiseWillReject() + { + $promise = new \React\Promise\Promise(function () { }, function ($resolve, $reject) { $reject(); }); + + $timeout = Timer\timeout($promise, 0.01, $this->loop); + + $promise->cancel(); + + $this->expectPromiseRejected($promise); + $this->expectPromiseRejected($timeout); + } + + public function testCancelTimeoutWillRejectIfGivenPromiseWillReject() + { + $promise = new \React\Promise\Promise(function () { }, function ($resolve, $reject) { $reject(); }); + + $timeout = Timer\timeout($promise, 0.01, $this->loop); + + $timeout->cancel(); + + $this->expectPromiseRejected($promise); + $this->expectPromiseRejected($timeout); + } + + public function testCancelTimeoutWillResolveIfGivenPromiseWillResolve() + { + $promise = new \React\Promise\Promise(function () { }, function ($resolve, $reject) { $resolve(); }); + + $timeout = Timer\timeout($promise, 0.01, $this->loop); + + $timeout->cancel(); + + $this->expectPromiseResolved($promise); + $this->expectPromiseResolved($timeout); + } +} diff --git a/assets/php/vendor/react/promise-timer/tests/TestCase.php b/assets/php/vendor/react/promise-timer/tests/TestCase.php new file mode 100644 index 0000000..9d8d49a --- /dev/null +++ b/assets/php/vendor/react/promise-timer/tests/TestCase.php @@ -0,0 +1,61 @@ +<?php + +namespace React\Tests\Promise\Timer; + +use PHPUnit\Framework\TestCase as BaseTestCase; +use React\EventLoop\Factory; + +class TestCase extends BaseTestCase +{ + protected $loop; + + public function setUp() + { + $this->loop = Factory::create(); + } + + protected function expectCallableOnce() + { + $mock = $this->createCallableMock(); + + $mock + ->expects($this->once()) + ->method('__invoke'); + + return $mock; + } + + protected function expectCallableNever() + { + $mock = $this->createCallableMock(); + + $mock + ->expects($this->never()) + ->method('__invoke'); + + return $mock; + } + + /** + * @link https://github.com/reactphp/react/blob/master/tests/React/Tests/Socket/TestCase.php (taken from reactphp/react) + */ + protected function createCallableMock() + { + return $this->getMockBuilder('React\Tests\Promise\Timer\CallableStub')->getMock(); + } + + protected function expectPromiseRejected($promise) + { + return $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); + } + + protected function expectPromiseResolved($promise) + { + return $promise->then($this->expectCallableOnce(), $this->expectCallableNever()); + } + + protected function expectPromisePending($promise) + { + return $promise->then($this->expectCallableNever(), $this->expectCallableNever()); + } +} diff --git a/assets/php/vendor/react/promise-timer/tests/TimeoutExceptionTest.php b/assets/php/vendor/react/promise-timer/tests/TimeoutExceptionTest.php new file mode 100644 index 0000000..e9bedd9 --- /dev/null +++ b/assets/php/vendor/react/promise-timer/tests/TimeoutExceptionTest.php @@ -0,0 +1,15 @@ +<?php + +namespace React\Tests\Promise\Timer; + +use React\Promise\Timer\TimeoutException; + +class TimeoutExceptionTest extends TestCase +{ + public function testAccessTimeout() + { + $e = new TimeoutException(10); + + $this->assertEquals(10, $e->getTimeout()); + } +} diff --git a/assets/php/vendor/react/promise/.gitignore b/assets/php/vendor/react/promise/.gitignore new file mode 100644 index 0000000..5241c60 --- /dev/null +++ b/assets/php/vendor/react/promise/.gitignore @@ -0,0 +1,5 @@ +composer.lock +composer.phar +phpunit.xml +build/ +vendor/ diff --git a/assets/php/vendor/react/promise/.travis.yml b/assets/php/vendor/react/promise/.travis.yml new file mode 100644 index 0000000..5d0c6ab --- /dev/null +++ b/assets/php/vendor/react/promise/.travis.yml @@ -0,0 +1,22 @@ +language: php + +php: + - 5.4 + - 5.5 + - 5.6 + - 7.0 + - nightly + - hhvm + +before_install: + - composer self-update + +install: + - composer install + +script: + - ./vendor/bin/phpunit -v --coverage-text --coverage-clover=./build/logs/clover.xml + +after_script: + - if [ -f ./build/logs/clover.xml ]; then travis_retry composer require satooshi/php-coveralls --no-interaction --update-with-dependencies; fi + - if [ -f ./build/logs/clover.xml ]; then php vendor/bin/coveralls -v; fi diff --git a/assets/php/vendor/react/promise/CHANGELOG.md b/assets/php/vendor/react/promise/CHANGELOG.md new file mode 100644 index 0000000..484e542 --- /dev/null +++ b/assets/php/vendor/react/promise/CHANGELOG.md @@ -0,0 +1,96 @@ +CHANGELOG for 2.x +================= + +* 2.5.1 (2017-03-25) + + * Fix circular references when resolving with a promise which follows + itself (#94). + +* 2.5.0 (2016-12-22) + + * Revert automatic cancellation of pending collection promises once the + output promise resolves. This was introduced in 42d86b7 (PR #36, released + in [v2.3.0](https://github.com/reactphp/promise/releases/tag/v2.3.0)) and + was both unintended and backward incompatible. + + If you need automatic cancellation, you can use something like: + + ```php + function allAndCancel(array $promises) + { + return \React\Promise\all($promises) + ->always(function() use ($promises) { + foreach ($promises as $promise) { + if ($promise instanceof \React\Promise\CancellablePromiseInterface) { + $promise->cancel(); + } + } + }); + } + ``` + * `all()` and `map()` functions now preserve the order of the array (#77). + * Fix circular references when resolving a promise with itself (#71). + +* 2.4.1 (2016-05-03) + + * Fix `some()` not cancelling pending promises when too much input promises + reject (16ff799). + +* 2.4.0 (2016-03-31) + + * Support foreign thenables in `resolve()`. + Any object that provides a `then()` method is now assimilated to a trusted + promise that follows the state of this thenable (#52). + * Fix `some()` and `any()` for input arrays containing not enough items + (#34). + +* 2.3.0 (2016-03-24) + + * Allow cancellation of promises returned by functions working on promise + collections (#36). + * Handle `\Throwable` in the same way as `\Exception` (#51 by @joshdifabio). + +* 2.2.2 (2016-02-26) + + * Fix cancellation handlers called multiple times (#47 by @clue). + +* 2.2.1 (2015-07-03) + + * Fix stack error when resolving a promise in its own fulfillment or + rejection handlers. + +* 2.2.0 (2014-12-30) + + * Introduce new `ExtendedPromiseInterface` implemented by all promises. + * Add new `done()` method (part of the `ExtendedPromiseInterface`). + * Add new `otherwise()` method (part of the `ExtendedPromiseInterface`). + * Add new `always()` method (part of the `ExtendedPromiseInterface`). + * Add new `progress()` method (part of the `ExtendedPromiseInterface`). + * Rename `Deferred::progress` to `Deferred::notify` to avoid confusion with + `ExtendedPromiseInterface::progress` (a `Deferred::progress` alias is + still available for backward compatibility) + * `resolve()` now always returns a `ExtendedPromiseInterface`. + +* 2.1.0 (2014-10-15) + + * Introduce new `CancellablePromiseInterface` implemented by all promises. + * Add new `cancel()` method (part of the `CancellablePromiseInterface`). + +* 2.0.0 (2013-12-10) + + New major release. The goal is to streamline the API and to make it more + compliant with other promise libraries and especially with the new upcoming + [ES6 promises specification](https://github.com/domenic/promises-unwrapping/). + + * Add standalone Promise class. + * Add new `race()` function. + * BC break: Bump minimum PHP version to PHP 5.4. + * BC break: Remove `ResolverInterface` and `PromiseInterface` from + `Deferred`. + * BC break: Change signature of `PromiseInterface`. + * BC break: Remove `When` and `Util` classes and move static methods to + functions. + * BC break: `FulfilledPromise` and `RejectedPromise` now throw an exception + when initialized with a promise instead of a value/reason. + * BC break: `Deferred::resolve()` and `Deferred::reject()` no longer return + a promise. diff --git a/assets/php/vendor/react/promise/LICENSE b/assets/php/vendor/react/promise/LICENSE new file mode 100644 index 0000000..5919d20 --- /dev/null +++ b/assets/php/vendor/react/promise/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2012-2016 Jan Sorgalla + +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/assets/php/vendor/react/promise/README.md b/assets/php/vendor/react/promise/README.md new file mode 100644 index 0000000..9c0558c --- /dev/null +++ b/assets/php/vendor/react/promise/README.md @@ -0,0 +1,840 @@ +React/Promise +============= + +A lightweight implementation of +[CommonJS Promises/A](http://wiki.commonjs.org/wiki/Promises/A) for PHP. + +[](http://travis-ci.org/reactphp/promise) +[](https://coveralls.io/github/reactphp/promise?branch=master) + +Table of Contents +----------------- + +1. [Introduction](#introduction) +2. [Concepts](#concepts) + * [Deferred](#deferred) + * [Promise](#promise) +3. [API](#api) + * [Deferred](#deferred-1) + * [Deferred::promise()](#deferredpromise) + * [Deferred::resolve()](#deferredresolve) + * [Deferred::reject()](#deferredreject) + * [Deferred::notify()](#deferrednotify) + * [PromiseInterface](#promiseinterface) + * [PromiseInterface::then()](#promiseinterfacethen) + * [ExtendedPromiseInterface](#extendedpromiseinterface) + * [ExtendedPromiseInterface::done()](#extendedpromiseinterfacedone) + * [ExtendedPromiseInterface::otherwise()](#extendedpromiseinterfaceotherwise) + * [ExtendedPromiseInterface::always()](#extendedpromiseinterfacealways) + * [ExtendedPromiseInterface::progress()](#extendedpromiseinterfaceprogress) + * [CancellablePromiseInterface](#cancellablepromiseinterface) + * [CancellablePromiseInterface::cancel()](#cancellablepromiseinterfacecancel) + * [Promise](#promise-1) + * [FulfilledPromise](#fulfilledpromise) + * [RejectedPromise](#rejectedpromise) + * [LazyPromise](#lazypromise) + * [Functions](#functions) + * [resolve()](#resolve) + * [reject()](#reject) + * [all()](#all) + * [race()](#race) + * [any()](#any) + * [some()](#some) + * [map()](#map) + * [reduce()](#reduce) + * [PromisorInterface](#promisorinterface) +4. [Examples](#examples) + * [How to use Deferred](#how-to-use-deferred) + * [How promise forwarding works](#how-promise-forwarding-works) + * [Resolution forwarding](#resolution-forwarding) + * [Rejection forwarding](#rejection-forwarding) + * [Mixed resolution and rejection forwarding](#mixed-resolution-and-rejection-forwarding) + * [Progress event forwarding](#progress-event-forwarding) + * [done() vs. then()](#done-vs-then) +5. [Credits](#credits) +6. [License](#license) + +Introduction +------------ + +React/Promise is a library implementing +[CommonJS Promises/A](http://wiki.commonjs.org/wiki/Promises/A) for PHP. + +It also provides several other useful promise-related concepts, such as joining +multiple promises and mapping and reducing collections of promises. + +If you've never heard about promises before, +[read this first](https://gist.github.com/3889970). + +Concepts +-------- + +### Deferred + +A **Deferred** represents a computation or unit of work that may not have +completed yet. Typically (but not always), that computation will be something +that executes asynchronously and completes at some point in the future. + +### Promise + +While a deferred represents the computation itself, a **Promise** represents +the result of that computation. Thus, each deferred has a promise that acts as +a placeholder for its actual result. + +API +--- + +### Deferred + +A deferred represents an operation whose resolution is pending. It has separate +promise and resolver parts. + +```php +$deferred = new React\Promise\Deferred(); + +$promise = $deferred->promise(); + +$deferred->resolve(mixed $value = null); +$deferred->reject(mixed $reason = null); +$deferred->notify(mixed $update = null); +``` + +The `promise` method returns the promise of the deferred. + +The `resolve` and `reject` methods control the state of the deferred. + +The `notify` method is for progress notification. + +The constructor of the `Deferred` accepts an optional `$canceller` argument. +See [Promise](#promise-1) for more information. + +#### Deferred::promise() + +```php +$promise = $deferred->promise(); +``` + +Returns the promise of the deferred, which you can hand out to others while +keeping the authority to modify its state to yourself. + +#### Deferred::resolve() + +```php +$deferred->resolve(mixed $value = null); +``` + +Resolves the promise returned by `promise()`. All consumers are notified by +having `$onFulfilled` (which they registered via `$promise->then()`) called with +`$value`. + +If `$value` itself is a promise, the promise will transition to the state of +this promise once it is resolved. + +#### Deferred::reject() + +```php +$deferred->reject(mixed $reason = null); +``` + +Rejects the promise returned by `promise()`, signalling that the deferred's +computation failed. +All consumers are notified by having `$onRejected` (which they registered via +`$promise->then()`) called with `$reason`. + +If `$reason` itself is a promise, the promise will be rejected with the outcome +of this promise regardless whether it fulfills or rejects. + +#### Deferred::notify() + +```php +$deferred->notify(mixed $update = null); +``` + +Triggers progress notifications, to indicate to consumers that the computation +is making progress toward its result. + +All consumers are notified by having `$onProgress` (which they registered via +`$promise->then()`) called with `$update`. + +### PromiseInterface + +The promise interface provides the common interface for all promise +implementations. + +A promise represents an eventual outcome, which is either fulfillment (success) +and an associated value, or rejection (failure) and an associated reason. + +Once in the fulfilled or rejected state, a promise becomes immutable. +Neither its state nor its result (or error) can be modified. + +#### Implementations + +* [Promise](#promise-1) +* [FulfilledPromise](#fulfilledpromise) +* [RejectedPromise](#rejectedpromise) +* [LazyPromise](#lazypromise) + +#### PromiseInterface::then() + +```php +$transformedPromise = $promise->then(callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null); +``` + +Transforms a promise's value by applying a function to the promise's fulfillment +or rejection value. Returns a new promise for the transformed result. + +The `then()` method registers new fulfilled, rejection and progress handlers +with a promise (all parameters are optional): + + * `$onFulfilled` will be invoked once the promise is fulfilled and passed + the result as the first argument. + * `$onRejected` will be invoked once the promise is rejected and passed the + reason as the first argument. + * `$onProgress` will be invoked whenever the producer of the promise + triggers progress notifications and passed a single argument (whatever it + wants) to indicate progress. + +It returns a new promise that will fulfill with the return value of either +`$onFulfilled` or `$onRejected`, whichever is called, or will reject with +the thrown exception if either throws. + +A promise makes the following guarantees about handlers registered in +the same call to `then()`: + + 1. Only one of `$onFulfilled` or `$onRejected` will be called, + never both. + 2. `$onFulfilled` and `$onRejected` will never be called more + than once. + 3. `$onProgress` may be called multiple times. + +#### See also + +* [resolve()](#resolve) - Creating a resolved promise +* [reject()](#reject) - Creating a rejected promise +* [ExtendedPromiseInterface::done()](#extendedpromiseinterfacedone) +* [done() vs. then()](#done-vs-then) + +### ExtendedPromiseInterface + +The ExtendedPromiseInterface extends the PromiseInterface with useful shortcut +and utility methods which are not part of the Promises/A specification. + +#### Implementations + +* [Promise](#promise-1) +* [FulfilledPromise](#fulfilledpromise) +* [RejectedPromise](#rejectedpromise) +* [LazyPromise](#lazypromise) + +#### ExtendedPromiseInterface::done() + +```php +$promise->done(callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null); +``` + +Consumes the promise's ultimate value if the promise fulfills, or handles the +ultimate error. + +It will cause a fatal error if either `$onFulfilled` or `$onRejected` throw or +return a rejected promise. + +Since the purpose of `done()` is consumption rather than transformation, +`done()` always returns `null`. + +#### See also + +* [PromiseInterface::then()](#promiseinterfacethen) +* [done() vs. then()](#done-vs-then) + +#### ExtendedPromiseInterface::otherwise() + +```php +$promise->otherwise(callable $onRejected); +``` + +Registers a rejection handler for promise. It is a shortcut for: + +```php +$promise->then(null, $onRejected); +``` + +Additionally, you can type hint the `$reason` argument of `$onRejected` to catch +only specific errors. + +```php +$promise + ->otherwise(function (\RuntimeException $reason) { + // Only catch \RuntimeException instances + // All other types of errors will propagate automatically + }) + ->otherwise(function ($reason) { + // Catch other errors + )}; +``` + +#### ExtendedPromiseInterface::always() + +```php +$newPromise = $promise->always(callable $onFulfilledOrRejected); +``` + +Allows you to execute "cleanup" type tasks in a promise chain. + +It arranges for `$onFulfilledOrRejected` to be called, with no arguments, +when the promise is either fulfilled or rejected. + +* If `$promise` fulfills, and `$onFulfilledOrRejected` returns successfully, + `$newPromise` will fulfill with the same value as `$promise`. +* If `$promise` fulfills, and `$onFulfilledOrRejected` throws or returns a + rejected promise, `$newPromise` will reject with the thrown exception or + rejected promise's reason. +* If `$promise` rejects, and `$onFulfilledOrRejected` returns successfully, + `$newPromise` will reject with the same reason as `$promise`. +* If `$promise` rejects, and `$onFulfilledOrRejected` throws or returns a + rejected promise, `$newPromise` will reject with the thrown exception or + rejected promise's reason. + +`always()` behaves similarly to the synchronous finally statement. When combined +with `otherwise()`, `always()` allows you to write code that is similar to the familiar +synchronous catch/finally pair. + +Consider the following synchronous code: + +```php +try { + return doSomething(); +} catch(\Exception $e) { + return handleError($e); +} finally { + cleanup(); +} +``` + +Similar asynchronous code (with `doSomething()` that returns a promise) can be +written: + +```php +return doSomething() + ->otherwise('handleError') + ->always('cleanup'); +``` + +#### ExtendedPromiseInterface::progress() + +```php +$promise->progress(callable $onProgress); +``` + +Registers a handler for progress updates from promise. It is a shortcut for: + +```php +$promise->then(null, null, $onProgress); +``` + +### CancellablePromiseInterface + +A cancellable promise provides a mechanism for consumers to notify the creator +of the promise that they are not longer interested in the result of an +operation. + +#### CancellablePromiseInterface::cancel() + +``` php +$promise->cancel(); +``` + +The `cancel()` method notifies the creator of the promise that there is no +further interest in the results of the operation. + +Once a promise is settled (either fulfilled or rejected), calling `cancel()` on +a promise has no effect. + +#### Implementations + +* [Promise](#promise-1) +* [FulfilledPromise](#fulfilledpromise) +* [RejectedPromise](#rejectedpromise) +* [LazyPromise](#lazypromise) + +### Promise + +Creates a promise whose state is controlled by the functions passed to +`$resolver`. + +```php +$resolver = function (callable $resolve, callable $reject, callable $notify) { + // Do some work, possibly asynchronously, and then + // resolve or reject. You can notify of progress events + // along the way if you want/need. + + $resolve($awesomeResult); + // or $resolve($anotherPromise); + // or $reject($nastyError); + // or $notify($progressNotification); +}; + +$canceller = function (callable $resolve, callable $reject, callable $progress) { + // Cancel/abort any running operations like network connections, streams etc. + + $reject(new \Exception('Promise cancelled')); +}; + +$promise = new React\Promise\Promise($resolver, $canceller); +``` + +The promise constructor receives a resolver function and an optional canceller +function which both will be called with 3 arguments: + + * `$resolve($value)` - Primary function that seals the fate of the + returned promise. Accepts either a non-promise value, or another promise. + When called with a non-promise value, fulfills promise with that value. + When called with another promise, e.g. `$resolve($otherPromise)`, promise's + fate will be equivalent to that of `$otherPromise`. + * `$reject($reason)` - Function that rejects the promise. + * `$notify($update)` - Function that issues progress events for the promise. + +If the resolver or canceller throw an exception, the promise will be rejected +with that thrown exception as the rejection reason. + +The resolver function will be called immediately, the canceller function only +once all consumers called the `cancel()` method of the promise. + +### FulfilledPromise + +Creates a already fulfilled promise. + +```php +$promise = React\Promise\FulfilledPromise($value); +``` + +Note, that `$value` **cannot** be a promise. It's recommended to use +[resolve()](#resolve) for creating resolved promises. + +### RejectedPromise + +Creates a already rejected promise. + +```php +$promise = React\Promise\RejectedPromise($reason); +``` + +Note, that `$reason` **cannot** be a promise. It's recommended to use +[reject()](#reject) for creating rejected promises. + +### LazyPromise + +Creates a promise which will be lazily initialized by `$factory` once a consumer +calls the `then()` method. + +```php +$factory = function () { + $deferred = new React\Promise\Deferred(); + + // Do some heavy stuff here and resolve the deferred once completed + + return $deferred->promise(); +}; + +$promise = React\Promise\LazyPromise($factory); + +// $factory will only be executed once we call then() +$promise->then(function ($value) { +}); +``` + +### Functions + +Useful functions for creating, joining, mapping and reducing collections of +promises. + +All functions working on promise collections (like `all()`, `race()`, `some()` +etc.) support cancellation. This means, if you call `cancel()` on the returned +promise, all promises in the collection are cancelled. If the collection itself +is a promise which resolves to an array, this promise is also cancelled. + +#### resolve() + +```php +$promise = React\Promise\resolve(mixed $promiseOrValue); +``` + +Creates a promise for the supplied `$promiseOrValue`. + +If `$promiseOrValue` is a value, it will be the resolution value of the +returned promise. + +If `$promiseOrValue` is a thenable (any object that provides a `then()` method), +a trusted promise that follows the state of the thenable is returned. + +If `$promiseOrValue` is a promise, it will be returned as is. + +Note: The promise returned is always a promise implementing +[ExtendedPromiseInterface](#extendedpromiseinterface). If you pass in a custom +promise which only implements [PromiseInterface](#promiseinterface), this +promise will be assimilated to a extended promise following `$promiseOrValue`. + +#### reject() + +```php +$promise = React\Promise\reject(mixed $promiseOrValue); +``` + +Creates a rejected promise for the supplied `$promiseOrValue`. + +If `$promiseOrValue` is a value, it will be the rejection value of the +returned promise. + +If `$promiseOrValue` is a promise, its completion value will be the rejected +value of the returned promise. + +This can be useful in situations where you need to reject a promise without +throwing an exception. For example, it allows you to propagate a rejection with +the value of another promise. + +#### all() + +```php +$promise = React\Promise\all(array|React\Promise\PromiseInterface $promisesOrValues); +``` + +Returns a promise that will resolve only once all the items in +`$promisesOrValues` have resolved. The resolution value of the returned promise +will be an array containing the resolution values of each of the items in +`$promisesOrValues`. + +#### race() + +```php +$promise = React\Promise\race(array|React\Promise\PromiseInterface $promisesOrValues); +``` + +Initiates a competitive race that allows one winner. Returns a promise which is +resolved in the same way the first settled promise resolves. + +#### any() + +```php +$promise = React\Promise\any(array|React\Promise\PromiseInterface $promisesOrValues); +``` + +Returns a promise that will resolve when any one of the items in +`$promisesOrValues` resolves. The resolution value of the returned promise +will be the resolution value of the triggering item. + +The returned promise will only reject if *all* items in `$promisesOrValues` are +rejected. The rejection value will be an array of all rejection reasons. + +The returned promise will also reject with a `React\Promise\Exception\LengthException` +if `$promisesOrValues` contains 0 items. + +#### some() + +```php +$promise = React\Promise\some(array|React\Promise\PromiseInterface $promisesOrValues, integer $howMany); +``` + +Returns a promise that will resolve when `$howMany` of the supplied items in +`$promisesOrValues` resolve. The resolution value of the returned promise +will be an array of length `$howMany` containing the resolution values of the +triggering items. + +The returned promise will reject if it becomes impossible for `$howMany` items +to resolve (that is, when `(count($promisesOrValues) - $howMany) + 1` items +reject). The rejection value will be an array of +`(count($promisesOrValues) - $howMany) + 1` rejection reasons. + +The returned promise will also reject with a `React\Promise\Exception\LengthException` +if `$promisesOrValues` contains less items than `$howMany`. + +#### map() + +```php +$promise = React\Promise\map(array|React\Promise\PromiseInterface $promisesOrValues, callable $mapFunc); +``` + +Traditional map function, similar to `array_map()`, but allows input to contain +promises and/or values, and `$mapFunc` may return either a value or a promise. + +The map function receives each item as argument, where item is a fully resolved +value of a promise or value in `$promisesOrValues`. + +#### reduce() + +```php +$promise = React\Promise\reduce(array|React\Promise\PromiseInterface $promisesOrValues, callable $reduceFunc , $initialValue = null); +``` + +Traditional reduce function, similar to `array_reduce()`, but input may contain +promises and/or values, and `$reduceFunc` may return either a value or a +promise, *and* `$initialValue` may be a promise or a value for the starting +value. + +### PromisorInterface + +The `React\Promise\PromisorInterface` provides a common interface for objects +that provide a promise. `React\Promise\Deferred` implements it, but since it +is part of the public API anyone can implement it. + +Examples +-------- + +### How to use Deferred + +```php +function getAwesomeResultPromise() +{ + $deferred = new React\Promise\Deferred(); + + // Execute a Node.js-style function using the callback pattern + computeAwesomeResultAsynchronously(function ($error, $result) use ($deferred) { + if ($error) { + $deferred->reject($error); + } else { + $deferred->resolve($result); + } + }); + + // Return the promise + return $deferred->promise(); +} + +getAwesomeResultPromise() + ->then( + function ($value) { + // Deferred resolved, do something with $value + }, + function ($reason) { + // Deferred rejected, do something with $reason + }, + function ($update) { + // Progress notification triggered, do something with $update + } + ); +``` + +### How promise forwarding works + +A few simple examples to show how the mechanics of Promises/A forwarding works. +These examples are contrived, of course, and in real usage, promise chains will +typically be spread across several function calls, or even several levels of +your application architecture. + +#### Resolution forwarding + +Resolved promises forward resolution values to the next promise. +The first promise, `$deferred->promise()`, will resolve with the value passed +to `$deferred->resolve()` below. + +Each call to `then()` returns a new promise that will resolve with the return +value of the previous handler. This creates a promise "pipeline". + +```php +$deferred = new React\Promise\Deferred(); + +$deferred->promise() + ->then(function ($x) { + // $x will be the value passed to $deferred->resolve() below + // and returns a *new promise* for $x + 1 + return $x + 1; + }) + ->then(function ($x) { + // $x === 2 + // This handler receives the return value of the + // previous handler. + return $x + 1; + }) + ->then(function ($x) { + // $x === 3 + // This handler receives the return value of the + // previous handler. + return $x + 1; + }) + ->then(function ($x) { + // $x === 4 + // This handler receives the return value of the + // previous handler. + echo 'Resolve ' . $x; + }); + +$deferred->resolve(1); // Prints "Resolve 4" +``` + +#### Rejection forwarding + +Rejected promises behave similarly, and also work similarly to try/catch: +When you catch an exception, you must rethrow for it to propagate. + +Similarly, when you handle a rejected promise, to propagate the rejection, +"rethrow" it by either returning a rejected promise, or actually throwing +(since promise translates thrown exceptions into rejections) + +```php +$deferred = new React\Promise\Deferred(); + +$deferred->promise() + ->then(function ($x) { + throw new \Exception($x + 1); + }) + ->otherwise(function (\Exception $x) { + // Propagate the rejection + throw $x; + }) + ->otherwise(function (\Exception $x) { + // Can also propagate by returning another rejection + return React\Promise\reject( + new \Exception($x->getMessage() + 1) + ); + }) + ->otherwise(function ($x) { + echo 'Reject ' . $x->getMessage(); // 3 + }); + +$deferred->resolve(1); // Prints "Reject 3" +``` + +#### Mixed resolution and rejection forwarding + +Just like try/catch, you can choose to propagate or not. Mixing resolutions and +rejections will still forward handler results in a predictable way. + +```php +$deferred = new React\Promise\Deferred(); + +$deferred->promise() + ->then(function ($x) { + return $x + 1; + }) + ->then(function ($x) { + throw new \Exception($x + 1); + }) + ->otherwise(function (\Exception $x) { + // Handle the rejection, and don't propagate. + // This is like catch without a rethrow + return $x->getMessage() + 1; + }) + ->then(function ($x) { + echo 'Mixed ' . $x; // 4 + }); + +$deferred->resolve(1); // Prints "Mixed 4" +``` + +#### Progress event forwarding + +In the same way as resolution and rejection handlers, your progress handler +**MUST** return a progress event to be propagated to the next link in the chain. +If you return nothing, `null` will be propagated. + +Also in the same way as resolutions and rejections, if you don't register a +progress handler, the update will be propagated through. + +If your progress handler throws an exception, the exception will be propagated +to the next link in the chain. The best thing to do is to ensure your progress +handlers do not throw exceptions. + +This gives you the opportunity to transform progress events at each step in the +chain so that they are meaningful to the next step. It also allows you to choose +not to transform them, and simply let them propagate untransformed, by not +registering a progress handler. + +```php +$deferred = new React\Promise\Deferred(); + +$deferred->promise() + ->progress(function ($update) { + return $update + 1; + }) + ->progress(function ($update) { + echo 'Progress ' . $update; // 2 + }); + +$deferred->notify(1); // Prints "Progress 2" +``` + +### done() vs. then() + +The golden rule is: + + Either return your promise, or call done() on it. + +At a first glance, `then()` and `done()` seem very similar. However, there are +important distinctions. + +The intent of `then()` is to transform a promise's value and to pass or return +a new promise for the transformed value along to other parts of your code. + +The intent of `done()` is to consume a promise's value, transferring +responsibility for the value to your code. + +In addition to transforming a value, `then()` allows you to recover from, or +propagate intermediate errors. Any errors that are not handled will be caught +by the promise machinery and used to reject the promise returned by `then()`. + +Calling `done()` transfers all responsibility for errors to your code. If an +error (either a thrown exception or returned rejection) escapes the +`$onFulfilled` or `$onRejected` callbacks you provide to done, it will be +rethrown in an uncatchable way causing a fatal error. + +```php +function getJsonResult() +{ + return queryApi() + ->then( + // Transform API results to an object + function ($jsonResultString) { + return json_decode($jsonResultString); + }, + // Transform API errors to an exception + function ($jsonErrorString) { + $object = json_decode($jsonErrorString); + throw new ApiErrorException($object->errorMessage); + } + ); +} + +// Here we provide no rejection handler. If the promise returned has been +// rejected, the ApiErrorException will be thrown +getJsonResult() + ->done( + // Consume transformed object + function ($jsonResultObject) { + // Do something with $jsonResultObject + } + ); + +// Here we provide a rejection handler which will either throw while debugging +// or log the exception +getJsonResult() + ->done( + function ($jsonResultObject) { + // Do something with $jsonResultObject + }, + function (ApiErrorException $exception) { + if (isDebug()) { + throw $exception; + } else { + logException($exception); + } + } + ); +``` + +Note that if a rejection value is not an instance of `\Exception`, it will be +wrapped in an exception of the type `React\Promise\UnhandledRejectionException`. + +You can get the original rejection reason by calling `$exception->getReason()`. + +Credits +------- + +React/Promise is a port of [when.js](https://github.com/cujojs/when) +by [Brian Cavalier](https://github.com/briancavalier). + +Also, large parts of the documentation have been ported from the when.js +[Wiki](https://github.com/cujojs/when/wiki) and the +[API docs](https://github.com/cujojs/when/blob/master/docs/api.md). + +License +------- + +React/Promise is released under the [MIT](https://github.com/reactphp/promise/blob/master/LICENSE) license. diff --git a/assets/php/vendor/react/promise/composer.json b/assets/php/vendor/react/promise/composer.json new file mode 100644 index 0000000..2fc4809 --- /dev/null +++ b/assets/php/vendor/react/promise/composer.json @@ -0,0 +1,29 @@ +{ + "name": "react/promise", + "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "license": "MIT", + "authors": [ + {"name": "Jan Sorgalla", "email": "jsorgalla@gmail.com"} + ], + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.8" + }, + "autoload": { + "psr-4": { + "React\\Promise\\": "src/" + }, + "files": ["src/functions_include.php"] + }, + "autoload-dev": { + "psr-4": { + "React\\Promise\\": "tests/fixtures" + } + }, + "keywords": [ + "promise", + "promises" + ] +} diff --git a/assets/php/vendor/react/promise/phpunit.xml.dist b/assets/php/vendor/react/promise/phpunit.xml.dist new file mode 100644 index 0000000..b9a689d --- /dev/null +++ b/assets/php/vendor/react/promise/phpunit.xml.dist @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<phpunit backupGlobals="false" + backupStaticAttributes="false" + colors="true" + convertErrorsToExceptions="true" + convertNoticesToExceptions="true" + convertWarningsToExceptions="true" + processIsolation="false" + stopOnFailure="false" + syntaxCheck="false" + bootstrap="tests/bootstrap.php" +> + <testsuites> + <testsuite name="Promise Test Suite"> + <directory>./tests/</directory> + </testsuite> + </testsuites> + + <filter> + <whitelist> + <directory>./src/</directory> + <exclude> + <file>./src/functions_include.php</file> + </exclude> + </whitelist> + </filter> +</phpunit> diff --git a/assets/php/vendor/react/promise/src/CancellablePromiseInterface.php b/assets/php/vendor/react/promise/src/CancellablePromiseInterface.php new file mode 100644 index 0000000..896db2d --- /dev/null +++ b/assets/php/vendor/react/promise/src/CancellablePromiseInterface.php @@ -0,0 +1,11 @@ +<?php + +namespace React\Promise; + +interface CancellablePromiseInterface extends PromiseInterface +{ + /** + * @return void + */ + public function cancel(); +} diff --git a/assets/php/vendor/react/promise/src/CancellationQueue.php b/assets/php/vendor/react/promise/src/CancellationQueue.php new file mode 100644 index 0000000..a366994 --- /dev/null +++ b/assets/php/vendor/react/promise/src/CancellationQueue.php @@ -0,0 +1,55 @@ +<?php + +namespace React\Promise; + +class CancellationQueue +{ + private $started = false; + private $queue = []; + + public function __invoke() + { + if ($this->started) { + return; + } + + $this->started = true; + $this->drain(); + } + + public function enqueue($cancellable) + { + if (!method_exists($cancellable, 'then') || !method_exists($cancellable, 'cancel')) { + return; + } + + $length = array_push($this->queue, $cancellable); + + if ($this->started && 1 === $length) { + $this->drain(); + } + } + + private function drain() + { + for ($i = key($this->queue); isset($this->queue[$i]); $i++) { + $cancellable = $this->queue[$i]; + + $exception = null; + + try { + $cancellable->cancel(); + } catch (\Throwable $exception) { + } catch (\Exception $exception) { + } + + unset($this->queue[$i]); + + if ($exception) { + throw $exception; + } + } + + $this->queue = []; + } +} diff --git a/assets/php/vendor/react/promise/src/Deferred.php b/assets/php/vendor/react/promise/src/Deferred.php new file mode 100644 index 0000000..f23980c --- /dev/null +++ b/assets/php/vendor/react/promise/src/Deferred.php @@ -0,0 +1,60 @@ +<?php + +namespace React\Promise; + +class Deferred implements PromisorInterface +{ + private $promise; + private $resolveCallback; + private $rejectCallback; + private $notifyCallback; + private $canceller; + + public function __construct(callable $canceller = null) + { + $this->canceller = $canceller; + } + + public function promise() + { + if (null === $this->promise) { + $this->promise = new Promise(function ($resolve, $reject, $notify) { + $this->resolveCallback = $resolve; + $this->rejectCallback = $reject; + $this->notifyCallback = $notify; + }, $this->canceller); + } + + return $this->promise; + } + + public function resolve($value = null) + { + $this->promise(); + + call_user_func($this->resolveCallback, $value); + } + + public function reject($reason = null) + { + $this->promise(); + + call_user_func($this->rejectCallback, $reason); + } + + public function notify($update = null) + { + $this->promise(); + + call_user_func($this->notifyCallback, $update); + } + + /** + * @deprecated 2.2.0 + * @see Deferred::notify() + */ + public function progress($update = null) + { + $this->notify($update); + } +} diff --git a/assets/php/vendor/react/promise/src/Exception/LengthException.php b/assets/php/vendor/react/promise/src/Exception/LengthException.php new file mode 100644 index 0000000..775c48d --- /dev/null +++ b/assets/php/vendor/react/promise/src/Exception/LengthException.php @@ -0,0 +1,7 @@ +<?php + +namespace React\Promise\Exception; + +class LengthException extends \LengthException +{ +} diff --git a/assets/php/vendor/react/promise/src/ExtendedPromiseInterface.php b/assets/php/vendor/react/promise/src/ExtendedPromiseInterface.php new file mode 100644 index 0000000..9cb6435 --- /dev/null +++ b/assets/php/vendor/react/promise/src/ExtendedPromiseInterface.php @@ -0,0 +1,26 @@ +<?php + +namespace React\Promise; + +interface ExtendedPromiseInterface extends PromiseInterface +{ + /** + * @return void + */ + public function done(callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null); + + /** + * @return ExtendedPromiseInterface + */ + public function otherwise(callable $onRejected); + + /** + * @return ExtendedPromiseInterface + */ + public function always(callable $onFulfilledOrRejected); + + /** + * @return ExtendedPromiseInterface + */ + public function progress(callable $onProgress); +} diff --git a/assets/php/vendor/react/promise/src/FulfilledPromise.php b/assets/php/vendor/react/promise/src/FulfilledPromise.php new file mode 100644 index 0000000..914bb5c --- /dev/null +++ b/assets/php/vendor/react/promise/src/FulfilledPromise.php @@ -0,0 +1,68 @@ +<?php + +namespace React\Promise; + +class FulfilledPromise implements ExtendedPromiseInterface, CancellablePromiseInterface +{ + private $value; + + public function __construct($value = null) + { + if ($value instanceof PromiseInterface) { + throw new \InvalidArgumentException('You cannot create React\Promise\FulfilledPromise with a promise. Use React\Promise\resolve($promiseOrValue) instead.'); + } + + $this->value = $value; + } + + public function then(callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null) + { + if (null === $onFulfilled) { + return $this; + } + + try { + return resolve($onFulfilled($this->value)); + } catch (\Throwable $exception) { + return new RejectedPromise($exception); + } catch (\Exception $exception) { + return new RejectedPromise($exception); + } + } + + public function done(callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null) + { + if (null === $onFulfilled) { + return; + } + + $result = $onFulfilled($this->value); + + if ($result instanceof ExtendedPromiseInterface) { + $result->done(); + } + } + + public function otherwise(callable $onRejected) + { + return $this; + } + + public function always(callable $onFulfilledOrRejected) + { + return $this->then(function ($value) use ($onFulfilledOrRejected) { + return resolve($onFulfilledOrRejected())->then(function () use ($value) { + return $value; + }); + }); + } + + public function progress(callable $onProgress) + { + return $this; + } + + public function cancel() + { + } +} diff --git a/assets/php/vendor/react/promise/src/LazyPromise.php b/assets/php/vendor/react/promise/src/LazyPromise.php new file mode 100644 index 0000000..7e3a3d3 --- /dev/null +++ b/assets/php/vendor/react/promise/src/LazyPromise.php @@ -0,0 +1,63 @@ +<?php + +namespace React\Promise; + +class LazyPromise implements ExtendedPromiseInterface, CancellablePromiseInterface +{ + private $factory; + private $promise; + + public function __construct(callable $factory) + { + $this->factory = $factory; + } + + public function then(callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null) + { + return $this->promise()->then($onFulfilled, $onRejected, $onProgress); + } + + public function done(callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null) + { + return $this->promise()->done($onFulfilled, $onRejected, $onProgress); + } + + public function otherwise(callable $onRejected) + { + return $this->promise()->otherwise($onRejected); + } + + public function always(callable $onFulfilledOrRejected) + { + return $this->promise()->always($onFulfilledOrRejected); + } + + public function progress(callable $onProgress) + { + return $this->promise()->progress($onProgress); + } + + public function cancel() + { + return $this->promise()->cancel(); + } + + /** + * @internal + * @see Promise::settle() + */ + public function promise() + { + if (null === $this->promise) { + try { + $this->promise = resolve(call_user_func($this->factory)); + } catch (\Throwable $exception) { + $this->promise = new RejectedPromise($exception); + } catch (\Exception $exception) { + $this->promise = new RejectedPromise($exception); + } + } + + return $this->promise; + } +} diff --git a/assets/php/vendor/react/promise/src/Promise.php b/assets/php/vendor/react/promise/src/Promise.php new file mode 100644 index 0000000..0261eb3 --- /dev/null +++ b/assets/php/vendor/react/promise/src/Promise.php @@ -0,0 +1,216 @@ +<?php + +namespace React\Promise; + +class Promise implements ExtendedPromiseInterface, CancellablePromiseInterface +{ + private $canceller; + private $result; + + private $handlers = []; + private $progressHandlers = []; + + private $requiredCancelRequests = 0; + private $cancelRequests = 0; + + public function __construct(callable $resolver, callable $canceller = null) + { + $this->canceller = $canceller; + $this->call($resolver); + } + + public function then(callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null) + { + if (null !== $this->result) { + return $this->result->then($onFulfilled, $onRejected, $onProgress); + } + + if (null === $this->canceller) { + return new static($this->resolver($onFulfilled, $onRejected, $onProgress)); + } + + $this->requiredCancelRequests++; + + return new static($this->resolver($onFulfilled, $onRejected, $onProgress), function () { + if (++$this->cancelRequests < $this->requiredCancelRequests) { + return; + } + + $this->cancel(); + }); + } + + public function done(callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null) + { + if (null !== $this->result) { + return $this->result->done($onFulfilled, $onRejected, $onProgress); + } + + $this->handlers[] = function (ExtendedPromiseInterface $promise) use ($onFulfilled, $onRejected) { + $promise + ->done($onFulfilled, $onRejected); + }; + + if ($onProgress) { + $this->progressHandlers[] = $onProgress; + } + } + + public function otherwise(callable $onRejected) + { + return $this->then(null, function ($reason) use ($onRejected) { + if (!_checkTypehint($onRejected, $reason)) { + return new RejectedPromise($reason); + } + + return $onRejected($reason); + }); + } + + public function always(callable $onFulfilledOrRejected) + { + return $this->then(function ($value) use ($onFulfilledOrRejected) { + return resolve($onFulfilledOrRejected())->then(function () use ($value) { + return $value; + }); + }, function ($reason) use ($onFulfilledOrRejected) { + return resolve($onFulfilledOrRejected())->then(function () use ($reason) { + return new RejectedPromise($reason); + }); + }); + } + + public function progress(callable $onProgress) + { + return $this->then(null, null, $onProgress); + } + + public function cancel() + { + if (null === $this->canceller || null !== $this->result) { + return; + } + + $canceller = $this->canceller; + $this->canceller = null; + + $this->call($canceller); + } + + private function resolver(callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null) + { + return function ($resolve, $reject, $notify) use ($onFulfilled, $onRejected, $onProgress) { + if ($onProgress) { + $progressHandler = function ($update) use ($notify, $onProgress) { + try { + $notify($onProgress($update)); + } catch (\Throwable $e) { + $notify($e); + } catch (\Exception $e) { + $notify($e); + } + }; + } else { + $progressHandler = $notify; + } + + $this->handlers[] = function (ExtendedPromiseInterface $promise) use ($onFulfilled, $onRejected, $resolve, $reject, $progressHandler) { + $promise + ->then($onFulfilled, $onRejected) + ->done($resolve, $reject, $progressHandler); + }; + + $this->progressHandlers[] = $progressHandler; + }; + } + + private function resolve($value = null) + { + if (null !== $this->result) { + return; + } + + $this->settle(resolve($value)); + } + + private function reject($reason = null) + { + if (null !== $this->result) { + return; + } + + $this->settle(reject($reason)); + } + + private function notify($update = null) + { + if (null !== $this->result) { + return; + } + + foreach ($this->progressHandlers as $handler) { + $handler($update); + } + } + + private function settle(ExtendedPromiseInterface $promise) + { + $promise = $this->unwrap($promise); + + $handlers = $this->handlers; + + $this->progressHandlers = $this->handlers = []; + $this->result = $promise; + + foreach ($handlers as $handler) { + $handler($promise); + } + } + + private function unwrap($promise) + { + $promise = $this->extract($promise); + + while ($promise instanceof self && null !== $promise->result) { + $promise = $this->extract($promise->result); + } + + return $promise; + } + + private function extract($promise) + { + if ($promise instanceof LazyPromise) { + $promise = $promise->promise(); + } + + if ($promise === $this) { + return new RejectedPromise( + new \LogicException('Cannot resolve a promise with itself.') + ); + } + + return $promise; + } + + private function call(callable $callback) + { + try { + $callback( + function ($value = null) { + $this->resolve($value); + }, + function ($reason = null) { + $this->reject($reason); + }, + function ($update = null) { + $this->notify($update); + } + ); + } catch (\Throwable $e) { + $this->reject($e); + } catch (\Exception $e) { + $this->reject($e); + } + } +} diff --git a/assets/php/vendor/react/promise/src/PromiseInterface.php b/assets/php/vendor/react/promise/src/PromiseInterface.php new file mode 100644 index 0000000..d80d114 --- /dev/null +++ b/assets/php/vendor/react/promise/src/PromiseInterface.php @@ -0,0 +1,11 @@ +<?php + +namespace React\Promise; + +interface PromiseInterface +{ + /** + * @return PromiseInterface + */ + public function then(callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null); +} diff --git a/assets/php/vendor/react/promise/src/PromisorInterface.php b/assets/php/vendor/react/promise/src/PromisorInterface.php new file mode 100644 index 0000000..9341a4f --- /dev/null +++ b/assets/php/vendor/react/promise/src/PromisorInterface.php @@ -0,0 +1,11 @@ +<?php + +namespace React\Promise; + +interface PromisorInterface +{ + /** + * @return PromiseInterface + */ + public function promise(); +} diff --git a/assets/php/vendor/react/promise/src/RejectedPromise.php b/assets/php/vendor/react/promise/src/RejectedPromise.php new file mode 100644 index 0000000..479a746 --- /dev/null +++ b/assets/php/vendor/react/promise/src/RejectedPromise.php @@ -0,0 +1,76 @@ +<?php + +namespace React\Promise; + +class RejectedPromise implements ExtendedPromiseInterface, CancellablePromiseInterface +{ + private $reason; + + public function __construct($reason = null) + { + if ($reason instanceof PromiseInterface) { + throw new \InvalidArgumentException('You cannot create React\Promise\RejectedPromise with a promise. Use React\Promise\reject($promiseOrValue) instead.'); + } + + $this->reason = $reason; + } + + public function then(callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null) + { + if (null === $onRejected) { + return $this; + } + + try { + return resolve($onRejected($this->reason)); + } catch (\Throwable $exception) { + return new RejectedPromise($exception); + } catch (\Exception $exception) { + return new RejectedPromise($exception); + } + } + + public function done(callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null) + { + if (null === $onRejected) { + throw UnhandledRejectionException::resolve($this->reason); + } + + $result = $onRejected($this->reason); + + if ($result instanceof self) { + throw UnhandledRejectionException::resolve($result->reason); + } + + if ($result instanceof ExtendedPromiseInterface) { + $result->done(); + } + } + + public function otherwise(callable $onRejected) + { + if (!_checkTypehint($onRejected, $this->reason)) { + return $this; + } + + return $this->then(null, $onRejected); + } + + public function always(callable $onFulfilledOrRejected) + { + return $this->then(null, function ($reason) use ($onFulfilledOrRejected) { + return resolve($onFulfilledOrRejected())->then(function () use ($reason) { + return new RejectedPromise($reason); + }); + }); + } + + public function progress(callable $onProgress) + { + return $this; + } + + public function cancel() + { + } +} diff --git a/assets/php/vendor/react/promise/src/UnhandledRejectionException.php b/assets/php/vendor/react/promise/src/UnhandledRejectionException.php new file mode 100644 index 0000000..a44b7a1 --- /dev/null +++ b/assets/php/vendor/react/promise/src/UnhandledRejectionException.php @@ -0,0 +1,31 @@ +<?php + +namespace React\Promise; + +class UnhandledRejectionException extends \RuntimeException +{ + private $reason; + + public static function resolve($reason) + { + if ($reason instanceof \Exception || $reason instanceof \Throwable) { + return $reason; + } + + return new static($reason); + } + + public function __construct($reason) + { + $this->reason = $reason; + + $message = sprintf('Unhandled Rejection: %s', json_encode($reason)); + + parent::__construct($message, 0); + } + + public function getReason() + { + return $this->reason; + } +} diff --git a/assets/php/vendor/react/promise/src/functions.php b/assets/php/vendor/react/promise/src/functions.php new file mode 100644 index 0000000..70c0eb7 --- /dev/null +++ b/assets/php/vendor/react/promise/src/functions.php @@ -0,0 +1,244 @@ +<?php + +namespace React\Promise; + +function resolve($promiseOrValue = null) +{ + if ($promiseOrValue instanceof ExtendedPromiseInterface) { + return $promiseOrValue; + } + + if (method_exists($promiseOrValue, 'then')) { + $canceller = null; + + if (method_exists($promiseOrValue, 'cancel')) { + $canceller = [$promiseOrValue, 'cancel']; + } + + return new Promise(function ($resolve, $reject, $notify) use ($promiseOrValue) { + $promiseOrValue->then($resolve, $reject, $notify); + }, $canceller); + } + + return new FulfilledPromise($promiseOrValue); +} + +function reject($promiseOrValue = null) +{ + if ($promiseOrValue instanceof PromiseInterface) { + return resolve($promiseOrValue)->then(function ($value) { + return new RejectedPromise($value); + }); + } + + return new RejectedPromise($promiseOrValue); +} + +function all($promisesOrValues) +{ + return map($promisesOrValues, function ($val) { + return $val; + }); +} + +function race($promisesOrValues) +{ + $cancellationQueue = new CancellationQueue(); + $cancellationQueue->enqueue($promisesOrValues); + + return new Promise(function ($resolve, $reject, $notify) use ($promisesOrValues, $cancellationQueue) { + resolve($promisesOrValues) + ->done(function ($array) use ($cancellationQueue, $resolve, $reject, $notify) { + if (!is_array($array) || !$array) { + $resolve(); + return; + } + + foreach ($array as $promiseOrValue) { + $cancellationQueue->enqueue($promiseOrValue); + + resolve($promiseOrValue) + ->done($resolve, $reject, $notify); + } + }, $reject, $notify); + }, $cancellationQueue); +} + +function any($promisesOrValues) +{ + return some($promisesOrValues, 1) + ->then(function ($val) { + return array_shift($val); + }); +} + +function some($promisesOrValues, $howMany) +{ + $cancellationQueue = new CancellationQueue(); + $cancellationQueue->enqueue($promisesOrValues); + + return new Promise(function ($resolve, $reject, $notify) use ($promisesOrValues, $howMany, $cancellationQueue) { + resolve($promisesOrValues) + ->done(function ($array) use ($howMany, $cancellationQueue, $resolve, $reject, $notify) { + if (!is_array($array) || $howMany < 1) { + $resolve([]); + return; + } + + $len = count($array); + + if ($len < $howMany) { + throw new Exception\LengthException( + sprintf( + 'Input array must contain at least %d item%s but contains only %s item%s.', + $howMany, + 1 === $howMany ? '' : 's', + $len, + 1 === $len ? '' : 's' + ) + ); + } + + $toResolve = $howMany; + $toReject = ($len - $toResolve) + 1; + $values = []; + $reasons = []; + + foreach ($array as $i => $promiseOrValue) { + $fulfiller = function ($val) use ($i, &$values, &$toResolve, $toReject, $resolve) { + if ($toResolve < 1 || $toReject < 1) { + return; + } + + $values[$i] = $val; + + if (0 === --$toResolve) { + $resolve($values); + } + }; + + $rejecter = function ($reason) use ($i, &$reasons, &$toReject, $toResolve, $reject) { + if ($toResolve < 1 || $toReject < 1) { + return; + } + + $reasons[$i] = $reason; + + if (0 === --$toReject) { + $reject($reasons); + } + }; + + $cancellationQueue->enqueue($promiseOrValue); + + resolve($promiseOrValue) + ->done($fulfiller, $rejecter, $notify); + } + }, $reject, $notify); + }, $cancellationQueue); +} + +function map($promisesOrValues, callable $mapFunc) +{ + $cancellationQueue = new CancellationQueue(); + $cancellationQueue->enqueue($promisesOrValues); + + return new Promise(function ($resolve, $reject, $notify) use ($promisesOrValues, $mapFunc, $cancellationQueue) { + resolve($promisesOrValues) + ->done(function ($array) use ($mapFunc, $cancellationQueue, $resolve, $reject, $notify) { + if (!is_array($array) || !$array) { + $resolve([]); + return; + } + + $toResolve = count($array); + $values = []; + + foreach ($array as $i => $promiseOrValue) { + $cancellationQueue->enqueue($promiseOrValue); + $values[$i] = null; + + resolve($promiseOrValue) + ->then($mapFunc) + ->done( + function ($mapped) use ($i, &$values, &$toResolve, $resolve) { + $values[$i] = $mapped; + + if (0 === --$toResolve) { + $resolve($values); + } + }, + $reject, + $notify + ); + } + }, $reject, $notify); + }, $cancellationQueue); +} + +function reduce($promisesOrValues, callable $reduceFunc, $initialValue = null) +{ + $cancellationQueue = new CancellationQueue(); + $cancellationQueue->enqueue($promisesOrValues); + + return new Promise(function ($resolve, $reject, $notify) use ($promisesOrValues, $reduceFunc, $initialValue, $cancellationQueue) { + resolve($promisesOrValues) + ->done(function ($array) use ($reduceFunc, $initialValue, $cancellationQueue, $resolve, $reject, $notify) { + if (!is_array($array)) { + $array = []; + } + + $total = count($array); + $i = 0; + + // Wrap the supplied $reduceFunc with one that handles promises and then + // delegates to the supplied. + $wrappedReduceFunc = function ($current, $val) use ($reduceFunc, $cancellationQueue, $total, &$i) { + $cancellationQueue->enqueue($val); + + return $current + ->then(function ($c) use ($reduceFunc, $total, &$i, $val) { + return resolve($val) + ->then(function ($value) use ($reduceFunc, $total, &$i, $c) { + return $reduceFunc($c, $value, $i++, $total); + }); + }); + }; + + $cancellationQueue->enqueue($initialValue); + + array_reduce($array, $wrappedReduceFunc, resolve($initialValue)) + ->done($resolve, $reject, $notify); + }, $reject, $notify); + }, $cancellationQueue); +} + +// Internal functions +function _checkTypehint(callable $callback, $object) +{ + if (!is_object($object)) { + return true; + } + + if (is_array($callback)) { + $callbackReflection = new \ReflectionMethod($callback[0], $callback[1]); + } elseif (is_object($callback) && !$callback instanceof \Closure) { + $callbackReflection = new \ReflectionMethod($callback, '__invoke'); + } else { + $callbackReflection = new \ReflectionFunction($callback); + } + + $parameters = $callbackReflection->getParameters(); + + if (!isset($parameters[0])) { + return true; + } + + $expectedException = $parameters[0]; + + if (!$expectedException->getClass()) { + return true; + } + + return $expectedException->getClass()->isInstance($object); +} diff --git a/assets/php/vendor/react/promise/src/functions_include.php b/assets/php/vendor/react/promise/src/functions_include.php new file mode 100644 index 0000000..c71decb --- /dev/null +++ b/assets/php/vendor/react/promise/src/functions_include.php @@ -0,0 +1,5 @@ +<?php + +if (!function_exists('React\Promise\resolve')) { + require __DIR__.'/functions.php'; +} diff --git a/assets/php/vendor/react/promise/tests/CancellationQueueTest.php b/assets/php/vendor/react/promise/tests/CancellationQueueTest.php new file mode 100644 index 0000000..32cedf4 --- /dev/null +++ b/assets/php/vendor/react/promise/tests/CancellationQueueTest.php @@ -0,0 +1,100 @@ +<?php + +namespace React\Promise; + +class CancellationQueueTest extends TestCase +{ + /** @test */ + public function acceptsSimpleCancellableThenable() + { + $p = new SimpleTestCancellableThenable(); + + $cancellationQueue = new CancellationQueue(); + $cancellationQueue->enqueue($p); + + $cancellationQueue(); + + $this->assertTrue($p->cancelCalled); + } + + /** @test */ + public function ignoresSimpleCancellable() + { + $p = new SimpleTestCancellable(); + + $cancellationQueue = new CancellationQueue(); + $cancellationQueue->enqueue($p); + + $cancellationQueue(); + + $this->assertFalse($p->cancelCalled); + } + + /** @test */ + public function callsCancelOnPromisesEnqueuedBeforeStart() + { + $d1 = $this->getCancellableDeferred(); + $d2 = $this->getCancellableDeferred(); + + $cancellationQueue = new CancellationQueue(); + $cancellationQueue->enqueue($d1->promise()); + $cancellationQueue->enqueue($d2->promise()); + + $cancellationQueue(); + } + + /** @test */ + public function callsCancelOnPromisesEnqueuedAfterStart() + { + $d1 = $this->getCancellableDeferred(); + $d2 = $this->getCancellableDeferred(); + + $cancellationQueue = new CancellationQueue(); + + $cancellationQueue(); + + $cancellationQueue->enqueue($d2->promise()); + $cancellationQueue->enqueue($d1->promise()); + } + + /** @test */ + public function doesNotCallCancelTwiceWhenStartedTwice() + { + $d = $this->getCancellableDeferred(); + + $cancellationQueue = new CancellationQueue(); + $cancellationQueue->enqueue($d->promise()); + + $cancellationQueue(); + $cancellationQueue(); + } + + /** @test */ + public function rethrowsExceptionsThrownFromCancel() + { + $this->setExpectedException('\Exception', 'test'); + + $mock = $this + ->getMockBuilder('React\Promise\CancellablePromiseInterface') + ->getMock(); + $mock + ->expects($this->once()) + ->method('cancel') + ->will($this->throwException(new \Exception('test'))); + + $cancellationQueue = new CancellationQueue(); + $cancellationQueue->enqueue($mock); + + $cancellationQueue(); + } + + private function getCancellableDeferred() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke'); + + return new Deferred($mock); + } +} diff --git a/assets/php/vendor/react/promise/tests/DeferredTest.php b/assets/php/vendor/react/promise/tests/DeferredTest.php new file mode 100644 index 0000000..16212e9 --- /dev/null +++ b/assets/php/vendor/react/promise/tests/DeferredTest.php @@ -0,0 +1,42 @@ +<?php + +namespace React\Promise; + +use React\Promise\PromiseAdapter\CallbackPromiseAdapter; + +class DeferredTest extends TestCase +{ + use PromiseTest\FullTestTrait; + + public function getPromiseTestAdapter(callable $canceller = null) + { + $d = new Deferred($canceller); + + return new CallbackPromiseAdapter([ + 'promise' => [$d, 'promise'], + 'resolve' => [$d, 'resolve'], + 'reject' => [$d, 'reject'], + 'notify' => [$d, 'progress'], + 'settle' => [$d, 'resolve'], + ]); + } + + /** @test */ + public function progressIsAnAliasForNotify() + { + $deferred = new Deferred(); + + $sentinel = new \stdClass(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($sentinel); + + $deferred->promise() + ->then($this->expectCallableNever(), $this->expectCallableNever(), $mock); + + $deferred->progress($sentinel); + } +} diff --git a/assets/php/vendor/react/promise/tests/FulfilledPromiseTest.php b/assets/php/vendor/react/promise/tests/FulfilledPromiseTest.php new file mode 100644 index 0000000..97fc8f6 --- /dev/null +++ b/assets/php/vendor/react/promise/tests/FulfilledPromiseTest.php @@ -0,0 +1,50 @@ +<?php + +namespace React\Promise; + +use React\Promise\PromiseAdapter\CallbackPromiseAdapter; + +class FulfilledPromiseTest extends TestCase +{ + use PromiseTest\PromiseSettledTestTrait, + PromiseTest\PromiseFulfilledTestTrait; + + public function getPromiseTestAdapter(callable $canceller = null) + { + $promise = null; + + return new CallbackPromiseAdapter([ + 'promise' => function () use (&$promise) { + if (!$promise) { + throw new \LogicException('FulfilledPromise must be resolved before obtaining the promise'); + } + + return $promise; + }, + 'resolve' => function ($value = null) use (&$promise) { + if (!$promise) { + $promise = new FulfilledPromise($value); + } + }, + 'reject' => function () { + throw new \LogicException('You cannot call reject() for React\Promise\FulfilledPromise'); + }, + 'notify' => function () { + // no-op + }, + 'settle' => function ($value = null) use (&$promise) { + if (!$promise) { + $promise = new FulfilledPromise($value); + } + }, + ]); + } + + /** @test */ + public function shouldThrowExceptionIfConstructedWithAPromise() + { + $this->setExpectedException('\InvalidArgumentException'); + + return new FulfilledPromise(new FulfilledPromise()); + } +} diff --git a/assets/php/vendor/react/promise/tests/FunctionAllTest.php b/assets/php/vendor/react/promise/tests/FunctionAllTest.php new file mode 100644 index 0000000..74c1d7c --- /dev/null +++ b/assets/php/vendor/react/promise/tests/FunctionAllTest.php @@ -0,0 +1,114 @@ +<?php + +namespace React\Promise; + +class FunctionAllTest extends TestCase +{ + /** @test */ + public function shouldResolveEmptyInput() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo([])); + + all([]) + ->then($mock); + } + + /** @test */ + public function shouldResolveValuesArray() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo([1, 2, 3])); + + all([1, 2, 3]) + ->then($mock); + } + + /** @test */ + public function shouldResolvePromisesArray() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo([1, 2, 3])); + + all([resolve(1), resolve(2), resolve(3)]) + ->then($mock); + } + + /** @test */ + public function shouldResolveSparseArrayInput() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo([null, 1, null, 1, 1])); + + all([null, 1, null, 1, 1]) + ->then($mock); + } + + /** @test */ + public function shouldRejectIfAnyInputPromiseRejects() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(2)); + + all([resolve(1), reject(2), resolve(3)]) + ->then($this->expectCallableNever(), $mock); + } + + /** @test */ + public function shouldAcceptAPromiseForAnArray() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo([1, 2, 3])); + + all(resolve([1, 2, 3])) + ->then($mock); + } + + /** @test */ + public function shouldResolveToEmptyArrayWhenInputPromiseDoesNotResolveToArray() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo([])); + + all(resolve(1)) + ->then($mock); + } + + /** @test */ + public function shouldPreserveTheOrderOfArrayWhenResolvingAsyncPromises() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo([1, 2, 3])); + + $deferred = new Deferred(); + + all([resolve(1), $deferred->promise(), resolve(3)]) + ->then($mock); + + $deferred->resolve(2); + } +} diff --git a/assets/php/vendor/react/promise/tests/FunctionAnyTest.php b/assets/php/vendor/react/promise/tests/FunctionAnyTest.php new file mode 100644 index 0000000..140b551 --- /dev/null +++ b/assets/php/vendor/react/promise/tests/FunctionAnyTest.php @@ -0,0 +1,204 @@ +<?php + +namespace React\Promise; + +use React\Promise\Exception\LengthException; + +class FunctionAnyTest extends TestCase +{ + /** @test */ + public function shouldRejectWithLengthExceptionWithEmptyInputArray() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with( + $this->callback(function($exception){ + return $exception instanceof LengthException && + 'Input array must contain at least 1 item but contains only 0 items.' === $exception->getMessage(); + }) + ); + + any([]) + ->then($this->expectCallableNever(), $mock); + } + + /** @test */ + public function shouldResolveToNullWithNonArrayInput() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(null)); + + any(null) + ->then($mock); + } + + /** @test */ + public function shouldResolveWithAnInputValue() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(1)); + + any([1, 2, 3]) + ->then($mock); + } + + /** @test */ + public function shouldResolveWithAPromisedInputValue() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(1)); + + any([resolve(1), resolve(2), resolve(3)]) + ->then($mock); + } + + /** @test */ + public function shouldRejectWithAllRejectedInputValuesIfAllInputsAreRejected() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo([0 => 1, 1 => 2, 2 => 3])); + + any([reject(1), reject(2), reject(3)]) + ->then($this->expectCallableNever(), $mock); + } + + /** @test */ + public function shouldResolveWhenFirstInputPromiseResolves() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(1)); + + any([resolve(1), reject(2), reject(3)]) + ->then($mock); + } + + /** @test */ + public function shouldAcceptAPromiseForAnArray() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(1)); + + any(resolve([1, 2, 3])) + ->then($mock); + } + + /** @test */ + public function shouldResolveToNullArrayWhenInputPromiseDoesNotResolveToArray() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(null)); + + any(resolve(1)) + ->then($mock); + } + + /** @test */ + public function shouldNotRelyOnArryIndexesWhenUnwrappingToASingleResolutionValue() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(2)); + + $d1 = new Deferred(); + $d2 = new Deferred(); + + any(['abc' => $d1->promise(), 1 => $d2->promise()]) + ->then($mock); + + $d2->resolve(2); + $d1->resolve(1); + } + + /** @test */ + public function shouldRejectWhenInputPromiseRejects() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(null)); + + any(reject()) + ->then($this->expectCallableNever(), $mock); + } + + /** @test */ + public function shouldCancelInputPromise() + { + $mock = $this + ->getMockBuilder('React\Promise\CancellablePromiseInterface') + ->getMock(); + $mock + ->expects($this->once()) + ->method('cancel'); + + any($mock)->cancel(); + } + + /** @test */ + public function shouldCancelInputArrayPromises() + { + $mock1 = $this + ->getMockBuilder('React\Promise\CancellablePromiseInterface') + ->getMock(); + $mock1 + ->expects($this->once()) + ->method('cancel'); + + $mock2 = $this + ->getMockBuilder('React\Promise\CancellablePromiseInterface') + ->getMock(); + $mock2 + ->expects($this->once()) + ->method('cancel'); + + any([$mock1, $mock2])->cancel(); + } + + /** @test */ + public function shouldNotCancelOtherPendingInputArrayPromisesIfOnePromiseFulfills() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->never()) + ->method('__invoke'); + + + $deferred = New Deferred($mock); + $deferred->resolve(); + + $mock2 = $this + ->getMockBuilder('React\Promise\CancellablePromiseInterface') + ->getMock(); + $mock2 + ->expects($this->never()) + ->method('cancel'); + + some([$deferred->promise(), $mock2], 1)->cancel(); + } +} diff --git a/assets/php/vendor/react/promise/tests/FunctionCheckTypehintTest.php b/assets/php/vendor/react/promise/tests/FunctionCheckTypehintTest.php new file mode 100644 index 0000000..8449bc1 --- /dev/null +++ b/assets/php/vendor/react/promise/tests/FunctionCheckTypehintTest.php @@ -0,0 +1,118 @@ +<?php + +namespace React\Promise; + +class FunctionCheckTypehintTest extends TestCase +{ + /** @test */ + public function shouldAcceptClosureCallbackWithTypehint() + { + $this->assertTrue(_checkTypehint(function (\InvalidArgumentException $e) { + }, new \InvalidArgumentException())); + $this->assertfalse(_checkTypehint(function (\InvalidArgumentException $e) { + }, new \Exception())); + } + + /** @test */ + public function shouldAcceptFunctionStringCallbackWithTypehint() + { + $this->assertTrue(_checkTypehint('React\Promise\testCallbackWithTypehint', new \InvalidArgumentException())); + $this->assertfalse(_checkTypehint('React\Promise\testCallbackWithTypehint', new \Exception())); + } + + /** @test */ + public function shouldAcceptInvokableObjectCallbackWithTypehint() + { + $this->assertTrue(_checkTypehint(new TestCallbackWithTypehintClass(), new \InvalidArgumentException())); + $this->assertfalse(_checkTypehint(new TestCallbackWithTypehintClass(), new \Exception())); + } + + /** @test */ + public function shouldAcceptObjectMethodCallbackWithTypehint() + { + $this->assertTrue(_checkTypehint([new TestCallbackWithTypehintClass(), 'testCallback'], new \InvalidArgumentException())); + $this->assertfalse(_checkTypehint([new TestCallbackWithTypehintClass(), 'testCallback'], new \Exception())); + } + + /** @test */ + public function shouldAcceptStaticClassCallbackWithTypehint() + { + $this->assertTrue(_checkTypehint(['React\Promise\TestCallbackWithTypehintClass', 'testCallbackStatic'], new \InvalidArgumentException())); + $this->assertfalse(_checkTypehint(['React\Promise\TestCallbackWithTypehintClass', 'testCallbackStatic'], new \Exception())); + } + + /** @test */ + public function shouldAcceptClosureCallbackWithoutTypehint() + { + $this->assertTrue(_checkTypehint(function (\InvalidArgumentException $e) { + }, new \InvalidArgumentException())); + } + + /** @test */ + public function shouldAcceptFunctionStringCallbackWithoutTypehint() + { + $this->assertTrue(_checkTypehint('React\Promise\testCallbackWithoutTypehint', new \InvalidArgumentException())); + } + + /** @test */ + public function shouldAcceptInvokableObjectCallbackWithoutTypehint() + { + $this->assertTrue(_checkTypehint(new TestCallbackWithoutTypehintClass(), new \InvalidArgumentException())); + } + + /** @test */ + public function shouldAcceptObjectMethodCallbackWithoutTypehint() + { + $this->assertTrue(_checkTypehint([new TestCallbackWithoutTypehintClass(), 'testCallback'], new \InvalidArgumentException())); + } + + /** @test */ + public function shouldAcceptStaticClassCallbackWithoutTypehint() + { + $this->assertTrue(_checkTypehint(['React\Promise\TestCallbackWithoutTypehintClass', 'testCallbackStatic'], new \InvalidArgumentException())); + } +} + +function testCallbackWithTypehint(\InvalidArgumentException $e) +{ +} + +function testCallbackWithoutTypehint() +{ +} + +class TestCallbackWithTypehintClass +{ + public function __invoke(\InvalidArgumentException $e) + { + + } + + public function testCallback(\InvalidArgumentException $e) + { + + } + + public static function testCallbackStatic(\InvalidArgumentException $e) + { + + } +} + +class TestCallbackWithoutTypehintClass +{ + public function __invoke() + { + + } + + public function testCallback() + { + + } + + public static function testCallbackStatic() + { + + } +} diff --git a/assets/php/vendor/react/promise/tests/FunctionMapTest.php b/assets/php/vendor/react/promise/tests/FunctionMapTest.php new file mode 100644 index 0000000..1ea560a --- /dev/null +++ b/assets/php/vendor/react/promise/tests/FunctionMapTest.php @@ -0,0 +1,198 @@ +<?php + +namespace React\Promise; + +class FunctionMapTest extends TestCase +{ + protected function mapper() + { + return function ($val) { + return $val * 2; + }; + } + + protected function promiseMapper() + { + return function ($val) { + return resolve($val * 2); + }; + } + + /** @test */ + public function shouldMapInputValuesArray() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo([2, 4, 6])); + + map( + [1, 2, 3], + $this->mapper() + )->then($mock); + } + + /** @test */ + public function shouldMapInputPromisesArray() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo([2, 4, 6])); + + map( + [resolve(1), resolve(2), resolve(3)], + $this->mapper() + )->then($mock); + } + + /** @test */ + public function shouldMapMixedInputArray() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo([2, 4, 6])); + + map( + [1, resolve(2), 3], + $this->mapper() + )->then($mock); + } + + /** @test */ + public function shouldMapInputWhenMapperReturnsAPromise() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo([2, 4, 6])); + + map( + [1, 2, 3], + $this->promiseMapper() + )->then($mock); + } + + /** @test */ + public function shouldAcceptAPromiseForAnArray() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo([2, 4, 6])); + + map( + resolve([1, resolve(2), 3]), + $this->mapper() + )->then($mock); + } + + /** @test */ + public function shouldResolveToEmptyArrayWhenInputPromiseDoesNotResolveToArray() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo([])); + + map( + resolve(1), + $this->mapper() + )->then($mock); + } + + /** @test */ + public function shouldPreserveTheOrderOfArrayWhenResolvingAsyncPromises() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo([2, 4, 6])); + + $deferred = new Deferred(); + + map( + [resolve(1), $deferred->promise(), resolve(3)], + $this->mapper() + )->then($mock); + + $deferred->resolve(2); + } + + /** @test */ + public function shouldRejectWhenInputContainsRejection() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(2)); + + map( + [resolve(1), reject(2), resolve(3)], + $this->mapper() + )->then($this->expectCallableNever(), $mock); + } + + /** @test */ + public function shouldRejectWhenInputPromiseRejects() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(null)); + + map( + reject(), + $this->mapper() + )->then($this->expectCallableNever(), $mock); + } + + /** @test */ + public function shouldCancelInputPromise() + { + $mock = $this + ->getMockBuilder('React\Promise\CancellablePromiseInterface') + ->getMock(); + $mock + ->expects($this->once()) + ->method('cancel'); + + map( + $mock, + $this->mapper() + )->cancel(); + } + + /** @test */ + public function shouldCancelInputArrayPromises() + { + $mock1 = $this + ->getMockBuilder('React\Promise\CancellablePromiseInterface') + ->getMock(); + $mock1 + ->expects($this->once()) + ->method('cancel'); + + $mock2 = $this + ->getMockBuilder('React\Promise\CancellablePromiseInterface') + ->getMock(); + $mock2 + ->expects($this->once()) + ->method('cancel'); + + map( + [$mock1, $mock2], + $this->mapper() + )->cancel(); + } +} diff --git a/assets/php/vendor/react/promise/tests/FunctionRaceTest.php b/assets/php/vendor/react/promise/tests/FunctionRaceTest.php new file mode 100644 index 0000000..83770ec --- /dev/null +++ b/assets/php/vendor/react/promise/tests/FunctionRaceTest.php @@ -0,0 +1,211 @@ +<?php + +namespace React\Promise; + +class FunctionRaceTest extends TestCase +{ + /** @test */ + public function shouldResolveEmptyInput() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(null)); + + race( + [] + )->then($mock); + } + + /** @test */ + public function shouldResolveValuesArray() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(1)); + + race( + [1, 2, 3] + )->then($mock); + } + + /** @test */ + public function shouldResolvePromisesArray() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(2)); + + $d1 = new Deferred(); + $d2 = new Deferred(); + $d3 = new Deferred(); + + race( + [$d1->promise(), $d2->promise(), $d3->promise()] + )->then($mock); + + $d2->resolve(2); + + $d1->resolve(1); + $d3->resolve(3); + } + + /** @test */ + public function shouldResolveSparseArrayInput() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(null)); + + race( + [null, 1, null, 2, 3] + )->then($mock); + } + + /** @test */ + public function shouldRejectIfFirstSettledPromiseRejects() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(2)); + + $d1 = new Deferred(); + $d2 = new Deferred(); + $d3 = new Deferred(); + + race( + [$d1->promise(), $d2->promise(), $d3->promise()] + )->then($this->expectCallableNever(), $mock); + + $d2->reject(2); + + $d1->resolve(1); + $d3->resolve(3); + } + + /** @test */ + public function shouldAcceptAPromiseForAnArray() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(1)); + + race( + resolve([1, 2, 3]) + )->then($mock); + } + + /** @test */ + public function shouldResolveToNullWhenInputPromiseDoesNotResolveToArray() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(null)); + + race( + resolve(1) + )->then($mock); + } + + /** @test */ + public function shouldRejectWhenInputPromiseRejects() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(null)); + + race( + reject() + )->then($this->expectCallableNever(), $mock); + } + + /** @test */ + public function shouldCancelInputPromise() + { + $mock = $this + ->getMockBuilder('React\Promise\CancellablePromiseInterface') + ->getMock(); + $mock + ->expects($this->once()) + ->method('cancel'); + + race($mock)->cancel(); + } + + /** @test */ + public function shouldCancelInputArrayPromises() + { + $mock1 = $this + ->getMockBuilder('React\Promise\CancellablePromiseInterface') + ->getMock(); + $mock1 + ->expects($this->once()) + ->method('cancel'); + + $mock2 = $this + ->getMockBuilder('React\Promise\CancellablePromiseInterface') + ->getMock(); + $mock2 + ->expects($this->once()) + ->method('cancel'); + + race([$mock1, $mock2])->cancel(); + } + + /** @test */ + public function shouldNotCancelOtherPendingInputArrayPromisesIfOnePromiseFulfills() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->never()) + ->method('__invoke'); + + $deferred = New Deferred($mock); + $deferred->resolve(); + + $mock2 = $this + ->getMockBuilder('React\Promise\CancellablePromiseInterface') + ->getMock(); + $mock2 + ->expects($this->never()) + ->method('cancel'); + + race([$deferred->promise(), $mock2])->cancel(); + } + + /** @test */ + public function shouldNotCancelOtherPendingInputArrayPromisesIfOnePromiseRejects() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->never()) + ->method('__invoke'); + + $deferred = New Deferred($mock); + $deferred->reject(); + + $mock2 = $this + ->getMockBuilder('React\Promise\CancellablePromiseInterface') + ->getMock(); + $mock2 + ->expects($this->never()) + ->method('cancel'); + + race([$deferred->promise(), $mock2])->cancel(); + } +} diff --git a/assets/php/vendor/react/promise/tests/FunctionReduceTest.php b/assets/php/vendor/react/promise/tests/FunctionReduceTest.php new file mode 100644 index 0000000..8b43a87 --- /dev/null +++ b/assets/php/vendor/react/promise/tests/FunctionReduceTest.php @@ -0,0 +1,347 @@ +<?php + +namespace React\Promise; + +class FunctionReduceTest extends TestCase +{ + protected function plus() + { + return function ($sum, $val) { + return $sum + $val; + }; + } + + protected function append() + { + return function ($sum, $val) { + return $sum . $val; + }; + } + + /** @test */ + public function shouldReduceValuesWithoutInitialValue() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(6)); + + reduce( + [1, 2, 3], + $this->plus() + )->then($mock); + } + + /** @test */ + public function shouldReduceValuesWithInitialValue() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(7)); + + reduce( + [1, 2, 3], + $this->plus(), + 1 + )->then($mock); + } + + /** @test */ + public function shouldReduceValuesWithInitialPromise() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(7)); + + reduce( + [1, 2, 3], + $this->plus(), + resolve(1) + )->then($mock); + } + + /** @test */ + public function shouldReducePromisedValuesWithoutInitialValue() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(6)); + + reduce( + [resolve(1), resolve(2), resolve(3)], + $this->plus() + )->then($mock); + } + + /** @test */ + public function shouldReducePromisedValuesWithInitialValue() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(7)); + + reduce( + [resolve(1), resolve(2), resolve(3)], + $this->plus(), + 1 + )->then($mock); + } + + /** @test */ + public function shouldReducePromisedValuesWithInitialPromise() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(7)); + + reduce( + [resolve(1), resolve(2), resolve(3)], + $this->plus(), + resolve(1) + )->then($mock); + } + + /** @test */ + public function shouldReduceEmptyInputWithInitialValue() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(1)); + + reduce( + [], + $this->plus(), + 1 + )->then($mock); + } + + /** @test */ + public function shouldReduceEmptyInputWithInitialPromise() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(1)); + + reduce( + [], + $this->plus(), + resolve(1) + )->then($mock); + } + + /** @test */ + public function shouldRejectWhenInputContainsRejection() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(2)); + + reduce( + [resolve(1), reject(2), resolve(3)], + $this->plus(), + resolve(1) + )->then($this->expectCallableNever(), $mock); + } + + /** @test */ + public function shouldResolveWithNullWhenInputIsEmptyAndNoInitialValueOrPromiseProvided() + { + // Note: this is different from when.js's behavior! + // In when.reduce(), this rejects with a TypeError exception (following + // JavaScript's [].reduce behavior. + // We're following PHP's array_reduce behavior and resolve with NULL. + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(null)); + + reduce( + [], + $this->plus() + )->then($mock); + } + + /** @test */ + public function shouldAllowSparseArrayInputWithoutInitialValue() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(3)); + + reduce( + [null, null, 1, null, 1, 1], + $this->plus() + )->then($mock); + } + + /** @test */ + public function shouldAllowSparseArrayInputWithInitialValue() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(4)); + + reduce( + [null, null, 1, null, 1, 1], + $this->plus(), + 1 + )->then($mock); + } + + /** @test */ + public function shouldReduceInInputOrder() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo('123')); + + reduce( + [1, 2, 3], + $this->append(), + '' + )->then($mock); + } + + /** @test */ + public function shouldAcceptAPromiseForAnArray() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo('123')); + + reduce( + resolve([1, 2, 3]), + $this->append(), + '' + )->then($mock); + } + + /** @test */ + public function shouldResolveToInitialValueWhenInputPromiseDoesNotResolveToAnArray() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(1)); + + reduce( + resolve(1), + $this->plus(), + 1 + )->then($mock); + } + + /** @test */ + public function shouldProvideCorrectBasisValue() + { + $insertIntoArray = function ($arr, $val, $i) { + $arr[$i] = $val; + + return $arr; + }; + + $d1 = new Deferred(); + $d2 = new Deferred(); + $d3 = new Deferred(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo([1, 2, 3])); + + reduce( + [$d1->promise(), $d2->promise(), $d3->promise()], + $insertIntoArray, + [] + )->then($mock); + + $d3->resolve(3); + $d1->resolve(1); + $d2->resolve(2); + } + + /** @test */ + public function shouldRejectWhenInputPromiseRejects() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(null)); + + reduce( + reject(), + $this->plus(), + 1 + )->then($this->expectCallableNever(), $mock); + } + + /** @test */ + public function shouldCancelInputPromise() + { + $mock = $this + ->getMockBuilder('React\Promise\CancellablePromiseInterface') + ->getMock(); + $mock + ->expects($this->once()) + ->method('cancel'); + + reduce( + $mock, + $this->plus(), + 1 + )->cancel(); + } + + /** @test */ + public function shouldCancelInputArrayPromises() + { + $mock1 = $this + ->getMockBuilder('React\Promise\CancellablePromiseInterface') + ->getMock(); + $mock1 + ->expects($this->once()) + ->method('cancel'); + + $mock2 = $this + ->getMockBuilder('React\Promise\CancellablePromiseInterface') + ->getMock(); + $mock2 + ->expects($this->once()) + ->method('cancel'); + + reduce( + [$mock1, $mock2], + $this->plus(), + 1 + )->cancel(); + } +} diff --git a/assets/php/vendor/react/promise/tests/FunctionRejectTest.php b/assets/php/vendor/react/promise/tests/FunctionRejectTest.php new file mode 100644 index 0000000..84b8ec6 --- /dev/null +++ b/assets/php/vendor/react/promise/tests/FunctionRejectTest.php @@ -0,0 +1,64 @@ +<?php + +namespace React\Promise; + +class FunctionRejectTest extends TestCase +{ + /** @test */ + public function shouldRejectAnImmediateValue() + { + $expected = 123; + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo($expected)); + + reject($expected) + ->then( + $this->expectCallableNever(), + $mock + ); + } + + /** @test */ + public function shouldRejectAFulfilledPromise() + { + $expected = 123; + + $resolved = new FulfilledPromise($expected); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo($expected)); + + reject($resolved) + ->then( + $this->expectCallableNever(), + $mock + ); + } + + /** @test */ + public function shouldRejectARejectedPromise() + { + $expected = 123; + + $resolved = new RejectedPromise($expected); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo($expected)); + + reject($resolved) + ->then( + $this->expectCallableNever(), + $mock + ); + } +} diff --git a/assets/php/vendor/react/promise/tests/FunctionResolveTest.php b/assets/php/vendor/react/promise/tests/FunctionResolveTest.php new file mode 100644 index 0000000..53126bc --- /dev/null +++ b/assets/php/vendor/react/promise/tests/FunctionResolveTest.php @@ -0,0 +1,171 @@ +<?php + +namespace React\Promise; + +class FunctionResolveTest extends TestCase +{ + /** @test */ + public function shouldResolveAnImmediateValue() + { + $expected = 123; + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo($expected)); + + resolve($expected) + ->then( + $mock, + $this->expectCallableNever() + ); + } + + /** @test */ + public function shouldResolveAFulfilledPromise() + { + $expected = 123; + + $resolved = new FulfilledPromise($expected); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo($expected)); + + resolve($resolved) + ->then( + $mock, + $this->expectCallableNever() + ); + } + + /** @test */ + public function shouldResolveAThenable() + { + $thenable = new SimpleFulfilledTestThenable(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo('foo')); + + resolve($thenable) + ->then( + $mock, + $this->expectCallableNever() + ); + } + + /** @test */ + public function shouldResolveACancellableThenable() + { + $thenable = new SimpleTestCancellableThenable(); + + $promise = resolve($thenable); + $promise->cancel(); + + $this->assertTrue($thenable->cancelCalled); + } + + /** @test */ + public function shouldRejectARejectedPromise() + { + $expected = 123; + + $resolved = new RejectedPromise($expected); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo($expected)); + + resolve($resolved) + ->then( + $this->expectCallableNever(), + $mock + ); + } + + /** @test */ + public function shouldSupportDeepNestingInPromiseChains() + { + $d = new Deferred(); + $d->resolve(false); + + $result = resolve(resolve($d->promise()->then(function ($val) { + $d = new Deferred(); + $d->resolve($val); + + $identity = function ($val) { + return $val; + }; + + return resolve($d->promise()->then($identity))->then( + function ($val) { + return !$val; + } + ); + }))); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(true)); + + $result->then($mock); + } + + /** @test */ + public function shouldSupportVeryDeepNestedPromises() + { + $deferreds = []; + + // @TODO Increase count once global-queue is merged + for ($i = 0; $i < 10; $i++) { + $deferreds[] = $d = new Deferred(); + $p = $d->promise(); + + $last = $p; + for ($j = 0; $j < 10; $j++) { + $last = $last->then(function($result) { + return $result; + }); + } + } + + $p = null; + foreach ($deferreds as $d) { + if ($p) { + $d->resolve($p); + } + + $p = $d->promise(); + } + + $deferreds[0]->resolve(true); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(true)); + + $deferreds[0]->promise()->then($mock); + } + + /** @test */ + public function returnsExtendePromiseForSimplePromise() + { + $promise = $this + ->getMockBuilder('React\Promise\PromiseInterface') + ->getMock(); + + $this->assertInstanceOf('React\Promise\ExtendedPromiseInterface', resolve($promise)); + } +} diff --git a/assets/php/vendor/react/promise/tests/FunctionSomeTest.php b/assets/php/vendor/react/promise/tests/FunctionSomeTest.php new file mode 100644 index 0000000..276b54b --- /dev/null +++ b/assets/php/vendor/react/promise/tests/FunctionSomeTest.php @@ -0,0 +1,258 @@ +<?php + +namespace React\Promise; + +use React\Promise\Exception\LengthException; + +class FunctionSomeTest extends TestCase +{ + /** @test */ + public function shouldRejectWithLengthExceptionWithEmptyInputArray() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with( + $this->callback(function($exception){ + return $exception instanceof LengthException && + 'Input array must contain at least 1 item but contains only 0 items.' === $exception->getMessage(); + }) + ); + + some( + [], + 1 + )->then($this->expectCallableNever(), $mock); + } + + /** @test */ + public function shouldRejectWithLengthExceptionWithInputArrayContainingNotEnoughItems() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with( + $this->callback(function($exception){ + return $exception instanceof LengthException && + 'Input array must contain at least 4 items but contains only 3 items.' === $exception->getMessage(); + }) + ); + + some( + [1, 2, 3], + 4 + )->then($this->expectCallableNever(), $mock); + } + + /** @test */ + public function shouldResolveToEmptyArrayWithNonArrayInput() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo([])); + + some( + null, + 1 + )->then($mock); + } + + /** @test */ + public function shouldResolveValuesArray() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo([1, 2])); + + some( + [1, 2, 3], + 2 + )->then($mock); + } + + /** @test */ + public function shouldResolvePromisesArray() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo([1, 2])); + + some( + [resolve(1), resolve(2), resolve(3)], + 2 + )->then($mock); + } + + /** @test */ + public function shouldResolveSparseArrayInput() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo([null, 1])); + + some( + [null, 1, null, 2, 3], + 2 + )->then($mock); + } + + /** @test */ + public function shouldRejectIfAnyInputPromiseRejectsBeforeDesiredNumberOfInputsAreResolved() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo([1 => 2, 2 => 3])); + + some( + [resolve(1), reject(2), reject(3)], + 2 + )->then($this->expectCallableNever(), $mock); + } + + /** @test */ + public function shouldAcceptAPromiseForAnArray() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo([1, 2])); + + some( + resolve([1, 2, 3]), + 2 + )->then($mock); + } + + /** @test */ + public function shouldResolveWithEmptyArrayIfHowManyIsLessThanOne() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo([])); + + some( + [1], + 0 + )->then($mock); + } + + /** @test */ + public function shouldResolveToEmptyArrayWhenInputPromiseDoesNotResolveToArray() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo([])); + + some( + resolve(1), + 1 + )->then($mock); + } + + /** @test */ + public function shouldRejectWhenInputPromiseRejects() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(null)); + + some( + reject(), + 1 + )->then($this->expectCallableNever(), $mock); + } + + /** @test */ + public function shouldCancelInputPromise() + { + $mock = $this + ->getMockBuilder('React\Promise\CancellablePromiseInterface') + ->getMock(); + $mock + ->expects($this->once()) + ->method('cancel'); + + some($mock, 1)->cancel(); + } + + /** @test */ + public function shouldCancelInputArrayPromises() + { + $mock1 = $this + ->getMockBuilder('React\Promise\CancellablePromiseInterface') + ->getMock(); + $mock1 + ->expects($this->once()) + ->method('cancel'); + + $mock2 = $this + ->getMockBuilder('React\Promise\CancellablePromiseInterface') + ->getMock(); + $mock2 + ->expects($this->once()) + ->method('cancel'); + + some([$mock1, $mock2], 1)->cancel(); + } + + /** @test */ + public function shouldNotCancelOtherPendingInputArrayPromisesIfEnoughPromisesFulfill() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->never()) + ->method('__invoke'); + + $deferred = New Deferred($mock); + $deferred->resolve(); + + $mock2 = $this + ->getMockBuilder('React\Promise\CancellablePromiseInterface') + ->getMock(); + $mock2 + ->expects($this->never()) + ->method('cancel'); + + some([$deferred->promise(), $mock2], 1); + } + + /** @test */ + public function shouldNotCancelOtherPendingInputArrayPromisesIfEnoughPromisesReject() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->never()) + ->method('__invoke'); + + $deferred = New Deferred($mock); + $deferred->reject(); + + $mock2 = $this + ->getMockBuilder('React\Promise\CancellablePromiseInterface') + ->getMock(); + $mock2 + ->expects($this->never()) + ->method('cancel'); + + some([$deferred->promise(), $mock2], 2); + } +} diff --git a/assets/php/vendor/react/promise/tests/LazyPromiseTest.php b/assets/php/vendor/react/promise/tests/LazyPromiseTest.php new file mode 100644 index 0000000..b630881 --- /dev/null +++ b/assets/php/vendor/react/promise/tests/LazyPromiseTest.php @@ -0,0 +1,107 @@ +<?php + +namespace React\Promise; + +use React\Promise\PromiseAdapter\CallbackPromiseAdapter; + +class LazyPromiseTest extends TestCase +{ + use PromiseTest\FullTestTrait; + + public function getPromiseTestAdapter(callable $canceller = null) + { + $d = new Deferred($canceller); + + $factory = function () use ($d) { + return $d->promise(); + }; + + return new CallbackPromiseAdapter([ + 'promise' => function () use ($factory) { + return new LazyPromise($factory); + }, + 'resolve' => [$d, 'resolve'], + 'reject' => [$d, 'reject'], + 'notify' => [$d, 'progress'], + 'settle' => [$d, 'resolve'], + ]); + } + + /** @test */ + public function shouldNotCallFactoryIfThenIsNotInvoked() + { + $factory = $this->createCallableMock(); + $factory + ->expects($this->never()) + ->method('__invoke'); + + new LazyPromise($factory); + } + + /** @test */ + public function shouldCallFactoryIfThenIsInvoked() + { + $factory = $this->createCallableMock(); + $factory + ->expects($this->once()) + ->method('__invoke'); + + $p = new LazyPromise($factory); + $p->then(); + } + + /** @test */ + public function shouldReturnPromiseFromFactory() + { + $factory = $this->createCallableMock(); + $factory + ->expects($this->once()) + ->method('__invoke') + ->will($this->returnValue(new FulfilledPromise(1))); + + $onFulfilled = $this->createCallableMock(); + $onFulfilled + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(1)); + + $p = new LazyPromise($factory); + + $p->then($onFulfilled); + } + + /** @test */ + public function shouldReturnPromiseIfFactoryReturnsNull() + { + $factory = $this->createCallableMock(); + $factory + ->expects($this->once()) + ->method('__invoke') + ->will($this->returnValue(null)); + + $p = new LazyPromise($factory); + $this->assertInstanceOf('React\\Promise\\PromiseInterface', $p->then()); + } + + /** @test */ + public function shouldReturnRejectedPromiseIfFactoryThrowsException() + { + $exception = new \Exception(); + + $factory = $this->createCallableMock(); + $factory + ->expects($this->once()) + ->method('__invoke') + ->will($this->throwException($exception)); + + $onRejected = $this->createCallableMock(); + $onRejected + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo($exception)); + + $p = new LazyPromise($factory); + + $p->then($this->expectCallableNever(), $onRejected); + } +} diff --git a/assets/php/vendor/react/promise/tests/PromiseAdapter/CallbackPromiseAdapter.php b/assets/php/vendor/react/promise/tests/PromiseAdapter/CallbackPromiseAdapter.php new file mode 100644 index 0000000..bdedf46 --- /dev/null +++ b/assets/php/vendor/react/promise/tests/PromiseAdapter/CallbackPromiseAdapter.php @@ -0,0 +1,40 @@ +<?php + +namespace React\Promise\PromiseAdapter; + +use React\Promise; + +class CallbackPromiseAdapter implements PromiseAdapterInterface +{ + private $callbacks; + + public function __construct(array $callbacks) + { + $this->callbacks = $callbacks; + } + + public function promise() + { + return call_user_func_array($this->callbacks['promise'], func_get_args()); + } + + public function resolve() + { + return call_user_func_array($this->callbacks['resolve'], func_get_args()); + } + + public function reject() + { + return call_user_func_array($this->callbacks['reject'], func_get_args()); + } + + public function notify() + { + return call_user_func_array($this->callbacks['notify'], func_get_args()); + } + + public function settle() + { + return call_user_func_array($this->callbacks['settle'], func_get_args()); + } +} diff --git a/assets/php/vendor/react/promise/tests/PromiseAdapter/PromiseAdapterInterface.php b/assets/php/vendor/react/promise/tests/PromiseAdapter/PromiseAdapterInterface.php new file mode 100644 index 0000000..9157cd4 --- /dev/null +++ b/assets/php/vendor/react/promise/tests/PromiseAdapter/PromiseAdapterInterface.php @@ -0,0 +1,14 @@ +<?php + +namespace React\Promise\PromiseAdapter; + +use React\Promise; + +interface PromiseAdapterInterface +{ + public function promise(); + public function resolve(); + public function reject(); + public function notify(); + public function settle(); +} diff --git a/assets/php/vendor/react/promise/tests/PromiseTest.php b/assets/php/vendor/react/promise/tests/PromiseTest.php new file mode 100644 index 0000000..dc7b733 --- /dev/null +++ b/assets/php/vendor/react/promise/tests/PromiseTest.php @@ -0,0 +1,84 @@ +<?php + +namespace React\Promise; + +use React\Promise\PromiseAdapter\CallbackPromiseAdapter; + +class PromiseTest extends TestCase +{ + use PromiseTest\FullTestTrait; + + public function getPromiseTestAdapter(callable $canceller = null) + { + $resolveCallback = $rejectCallback = $progressCallback = null; + + $promise = new Promise(function ($resolve, $reject, $progress) use (&$resolveCallback, &$rejectCallback, &$progressCallback) { + $resolveCallback = $resolve; + $rejectCallback = $reject; + $progressCallback = $progress; + }, $canceller); + + return new CallbackPromiseAdapter([ + 'promise' => function () use ($promise) { + return $promise; + }, + 'resolve' => $resolveCallback, + 'reject' => $rejectCallback, + 'notify' => $progressCallback, + 'settle' => $resolveCallback, + ]); + } + + /** @test */ + public function shouldRejectIfResolverThrowsException() + { + $exception = new \Exception('foo'); + + $promise = new Promise(function () use ($exception) { + throw $exception; + }); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo($exception)); + + $promise + ->then($this->expectCallableNever(), $mock); + } + + /** @test */ + public function shouldFulfillIfFullfilledWithSimplePromise() + { + $adapter = $this->getPromiseTestAdapter(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo('foo')); + + $adapter->promise() + ->then($mock); + + $adapter->resolve(new SimpleFulfilledTestPromise()); + } + + /** @test */ + public function shouldRejectIfRejectedWithSimplePromise() + { + $adapter = $this->getPromiseTestAdapter(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo('foo')); + + $adapter->promise() + ->then($this->expectCallableNever(), $mock); + + $adapter->resolve(new SimpleRejectedTestPromise()); + } +} diff --git a/assets/php/vendor/react/promise/tests/PromiseTest/CancelTestTrait.php b/assets/php/vendor/react/promise/tests/PromiseTest/CancelTestTrait.php new file mode 100644 index 0000000..d722d75 --- /dev/null +++ b/assets/php/vendor/react/promise/tests/PromiseTest/CancelTestTrait.php @@ -0,0 +1,231 @@ +<?php + +namespace React\Promise\PromiseTest; + +use React\Promise; + +trait CancelTestTrait +{ + /** + * @return \React\Promise\PromiseAdapter\PromiseAdapterInterface + */ + abstract public function getPromiseTestAdapter(callable $canceller = null); + + /** @test */ + public function cancelShouldCallCancellerWithResolverArguments() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->isType('callable'), $this->isType('callable'), $this->isType('callable')); + + $adapter = $this->getPromiseTestAdapter($mock); + + $adapter->promise()->cancel(); + } + + /** @test */ + public function cancelShouldFulfillPromiseIfCancellerFulfills() + { + $adapter = $this->getPromiseTestAdapter(function ($resolve) { + $resolve(1); + }); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(1)); + + $adapter->promise() + ->then($mock, $this->expectCallableNever()); + + $adapter->promise()->cancel(); + } + + /** @test */ + public function cancelShouldRejectPromiseIfCancellerRejects() + { + $adapter = $this->getPromiseTestAdapter(function ($resolve, $reject) { + $reject(1); + }); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(1)); + + $adapter->promise() + ->then($this->expectCallableNever(), $mock); + + $adapter->promise()->cancel(); + } + + /** @test */ + public function cancelShouldRejectPromiseWithExceptionIfCancellerThrows() + { + $e = new \Exception(); + + $adapter = $this->getPromiseTestAdapter(function () use ($e) { + throw $e; + }); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo($e)); + + $adapter->promise() + ->then($this->expectCallableNever(), $mock); + + $adapter->promise()->cancel(); + } + + /** @test */ + public function cancelShouldProgressPromiseIfCancellerNotifies() + { + $adapter = $this->getPromiseTestAdapter(function ($resolve, $reject, $progress) { + $progress(1); + }); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(1)); + + $adapter->promise() + ->then($this->expectCallableNever(), $this->expectCallableNever(), $mock); + + $adapter->promise()->cancel(); + } + + /** @test */ + public function cancelShouldCallCancellerOnlyOnceIfCancellerResolves() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->will($this->returnCallback(function ($resolve) { + $resolve(); + })); + + $adapter = $this->getPromiseTestAdapter($mock); + + $adapter->promise()->cancel(); + $adapter->promise()->cancel(); + } + + /** @test */ + public function cancelShouldHaveNoEffectIfCancellerDoesNothing() + { + $adapter = $this->getPromiseTestAdapter(function () {}); + + $adapter->promise() + ->then($this->expectCallableNever(), $this->expectCallableNever()); + + $adapter->promise()->cancel(); + $adapter->promise()->cancel(); + } + + /** @test */ + public function cancelShouldCallCancellerFromDeepNestedPromiseChain() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke'); + + $adapter = $this->getPromiseTestAdapter($mock); + + $promise = $adapter->promise() + ->then(function () { + return new Promise\Promise(function () {}); + }) + ->then(function () { + $d = new Promise\Deferred(); + + return $d->promise(); + }) + ->then(function () { + return new Promise\Promise(function () {}); + }); + + $promise->cancel(); + } + + /** @test */ + public function cancelCalledOnChildrenSouldOnlyCancelWhenAllChildrenCancelled() + { + $adapter = $this->getPromiseTestAdapter($this->expectCallableNever()); + + $child1 = $adapter->promise() + ->then() + ->then(); + + $adapter->promise() + ->then(); + + $child1->cancel(); + } + + /** @test */ + public function cancelShouldTriggerCancellerWhenAllChildrenCancel() + { + $adapter = $this->getPromiseTestAdapter($this->expectCallableOnce()); + + $child1 = $adapter->promise() + ->then() + ->then(); + + $child2 = $adapter->promise() + ->then(); + + $child1->cancel(); + $child2->cancel(); + } + + /** @test */ + public function cancelShouldNotTriggerCancellerWhenCancellingOneChildrenMultipleTimes() + { + $adapter = $this->getPromiseTestAdapter($this->expectCallableNever()); + + $child1 = $adapter->promise() + ->then() + ->then(); + + $child2 = $adapter->promise() + ->then(); + + $child1->cancel(); + $child1->cancel(); + } + + /** @test */ + public function cancelShouldTriggerCancellerOnlyOnceWhenCancellingMultipleTimes() + { + $adapter = $this->getPromiseTestAdapter($this->expectCallableOnce()); + + $adapter->promise()->cancel(); + $adapter->promise()->cancel(); + } + + /** @test */ + public function cancelShouldAlwaysTriggerCancellerWhenCalledOnRootPromise() + { + $adapter = $this->getPromiseTestAdapter($this->expectCallableOnce()); + + $adapter->promise() + ->then() + ->then(); + + $adapter->promise() + ->then(); + + $adapter->promise()->cancel(); + } +} diff --git a/assets/php/vendor/react/promise/tests/PromiseTest/FullTestTrait.php b/assets/php/vendor/react/promise/tests/PromiseTest/FullTestTrait.php new file mode 100644 index 0000000..3ce45d6 --- /dev/null +++ b/assets/php/vendor/react/promise/tests/PromiseTest/FullTestTrait.php @@ -0,0 +1,15 @@ +<?php + +namespace React\Promise\PromiseTest; + +trait FullTestTrait +{ + use PromisePendingTestTrait, + PromiseSettledTestTrait, + PromiseFulfilledTestTrait, + PromiseRejectedTestTrait, + ResolveTestTrait, + RejectTestTrait, + NotifyTestTrait, + CancelTestTrait; +} diff --git a/assets/php/vendor/react/promise/tests/PromiseTest/NotifyTestTrait.php b/assets/php/vendor/react/promise/tests/PromiseTest/NotifyTestTrait.php new file mode 100644 index 0000000..4501df6 --- /dev/null +++ b/assets/php/vendor/react/promise/tests/PromiseTest/NotifyTestTrait.php @@ -0,0 +1,336 @@ +<?php + +namespace React\Promise\PromiseTest; + +trait NotifyTestTrait +{ + /** + * @return \React\Promise\PromiseAdapter\PromiseAdapterInterface + */ + abstract public function getPromiseTestAdapter(callable $canceller = null); + + /** @test */ + public function notifyShouldProgress() + { + $adapter = $this->getPromiseTestAdapter(); + + $sentinel = new \stdClass(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($sentinel); + + $adapter->promise() + ->then($this->expectCallableNever(), $this->expectCallableNever(), $mock); + + $adapter->notify($sentinel); + } + + /** @test */ + public function notifyShouldPropagateProgressToDownstreamPromises() + { + $adapter = $this->getPromiseTestAdapter(); + + $sentinel = new \stdClass(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->will($this->returnArgument(0)); + + $mock2 = $this->createCallableMock(); + $mock2 + ->expects($this->once()) + ->method('__invoke') + ->with($sentinel); + + $adapter->promise() + ->then( + $this->expectCallableNever(), + $this->expectCallableNever(), + $mock + ) + ->then( + $this->expectCallableNever(), + $this->expectCallableNever(), + $mock2 + ); + + $adapter->notify($sentinel); + } + + /** @test */ + public function notifyShouldPropagateTransformedProgressToDownstreamPromises() + { + $adapter = $this->getPromiseTestAdapter(); + + $sentinel = new \stdClass(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->will($this->returnValue($sentinel)); + + $mock2 = $this->createCallableMock(); + $mock2 + ->expects($this->once()) + ->method('__invoke') + ->with($sentinel); + + $adapter->promise() + ->then( + $this->expectCallableNever(), + $this->expectCallableNever(), + $mock + ) + ->then( + $this->expectCallableNever(), + $this->expectCallableNever(), + $mock2 + ); + + $adapter->notify(1); + } + + /** @test */ + public function notifyShouldPropagateCaughtExceptionValueAsProgress() + { + $adapter = $this->getPromiseTestAdapter(); + + $exception = new \Exception(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->will($this->throwException($exception)); + + $mock2 = $this->createCallableMock(); + $mock2 + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo($exception)); + + $adapter->promise() + ->then( + $this->expectCallableNever(), + $this->expectCallableNever(), + $mock + ) + ->then( + $this->expectCallableNever(), + $this->expectCallableNever(), + $mock2 + ); + + $adapter->notify(1); + } + + /** @test */ + public function notifyShouldForwardProgressEventsWhenIntermediaryCallbackTiedToAResolvedPromiseReturnsAPromise() + { + $adapter = $this->getPromiseTestAdapter(); + $adapter2 = $this->getPromiseTestAdapter(); + + $promise2 = $adapter2->promise(); + + $sentinel = new \stdClass(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($sentinel); + + // resolve BEFORE attaching progress handler + $adapter->resolve(); + + $adapter->promise() + ->then(function () use ($promise2) { + return $promise2; + }) + ->then( + $this->expectCallableNever(), + $this->expectCallableNever(), + $mock + ); + + $adapter2->notify($sentinel); + } + + /** @test */ + public function notifyShouldForwardProgressEventsWhenIntermediaryCallbackTiedToAnUnresolvedPromiseReturnsAPromise() + { + $adapter = $this->getPromiseTestAdapter(); + $adapter2 = $this->getPromiseTestAdapter(); + + $promise2 = $adapter2->promise(); + + $sentinel = new \stdClass(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($sentinel); + + $adapter->promise() + ->then(function () use ($promise2) { + return $promise2; + }) + ->then( + $this->expectCallableNever(), + $this->expectCallableNever(), + $mock + ); + + // resolve AFTER attaching progress handler + $adapter->resolve(); + $adapter2->notify($sentinel); + } + + /** @test */ + public function notifyShouldForwardProgressWhenResolvedWithAnotherPromise() + { + $adapter = $this->getPromiseTestAdapter(); + $adapter2 = $this->getPromiseTestAdapter(); + + $sentinel = new \stdClass(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->will($this->returnValue($sentinel)); + + $mock2 = $this->createCallableMock(); + $mock2 + ->expects($this->once()) + ->method('__invoke') + ->with($sentinel); + + $adapter->promise() + ->then( + $this->expectCallableNever(), + $this->expectCallableNever(), + $mock + ) + ->then( + $this->expectCallableNever(), + $this->expectCallableNever(), + $mock2 + ); + + $adapter->resolve($adapter2->promise()); + $adapter2->notify($sentinel); + } + + /** @test */ + public function notifyShouldAllowResolveAfterProgress() + { + $adapter = $this->getPromiseTestAdapter(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->at(0)) + ->method('__invoke') + ->with($this->identicalTo(1)); + $mock + ->expects($this->at(1)) + ->method('__invoke') + ->with($this->identicalTo(2)); + + $adapter->promise() + ->then( + $mock, + $this->expectCallableNever(), + $mock + ); + + $adapter->notify(1); + $adapter->resolve(2); + } + + /** @test */ + public function notifyShouldAllowRejectAfterProgress() + { + $adapter = $this->getPromiseTestAdapter(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->at(0)) + ->method('__invoke') + ->with($this->identicalTo(1)); + $mock + ->expects($this->at(1)) + ->method('__invoke') + ->with($this->identicalTo(2)); + + $adapter->promise() + ->then( + $this->expectCallableNever(), + $mock, + $mock + ); + + $adapter->notify(1); + $adapter->reject(2); + } + + /** @test */ + public function notifyShouldReturnSilentlyOnProgressWhenAlreadyRejected() + { + $adapter = $this->getPromiseTestAdapter(); + + $adapter->reject(1); + + $this->assertNull($adapter->notify()); + } + + /** @test */ + public function notifyShouldInvokeProgressHandler() + { + $adapter = $this->getPromiseTestAdapter(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(1)); + + $adapter->promise()->progress($mock); + $adapter->notify(1); + } + + /** @test */ + public function notifyShouldInvokeProgressHandlerFromDone() + { + $adapter = $this->getPromiseTestAdapter(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(1)); + + $this->assertNull($adapter->promise()->done(null, null, $mock)); + $adapter->notify(1); + } + + /** @test */ + public function notifyShouldThrowExceptionThrownProgressHandlerFromDone() + { + $adapter = $this->getPromiseTestAdapter(); + + $this->setExpectedException('\Exception', 'UnhandledRejectionException'); + + $this->assertNull($adapter->promise()->done(null, null, function () { + throw new \Exception('UnhandledRejectionException'); + })); + $adapter->notify(1); + } +} diff --git a/assets/php/vendor/react/promise/tests/PromiseTest/PromiseFulfilledTestTrait.php b/assets/php/vendor/react/promise/tests/PromiseTest/PromiseFulfilledTestTrait.php new file mode 100644 index 0000000..428230b --- /dev/null +++ b/assets/php/vendor/react/promise/tests/PromiseTest/PromiseFulfilledTestTrait.php @@ -0,0 +1,351 @@ +<?php + +namespace React\Promise\PromiseTest; + +trait PromiseFulfilledTestTrait +{ + /** + * @return \React\Promise\PromiseAdapter\PromiseAdapterInterface + */ + abstract public function getPromiseTestAdapter(callable $canceller = null); + + /** @test */ + public function fulfilledPromiseShouldBeImmutable() + { + $adapter = $this->getPromiseTestAdapter(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(1)); + + $adapter->resolve(1); + $adapter->resolve(2); + + $adapter->promise() + ->then( + $mock, + $this->expectCallableNever() + ); + } + + /** @test */ + public function fulfilledPromiseShouldInvokeNewlyAddedCallback() + { + $adapter = $this->getPromiseTestAdapter(); + + $adapter->resolve(1); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(1)); + + $adapter->promise() + ->then($mock, $this->expectCallableNever()); + } + + /** @test */ + public function thenShouldForwardResultWhenCallbackIsNull() + { + $adapter = $this->getPromiseTestAdapter(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(1)); + + $adapter->resolve(1); + $adapter->promise() + ->then( + null, + $this->expectCallableNever() + ) + ->then( + $mock, + $this->expectCallableNever() + ); + } + + /** @test */ + public function thenShouldForwardCallbackResultToNextCallback() + { + $adapter = $this->getPromiseTestAdapter(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(2)); + + $adapter->resolve(1); + $adapter->promise() + ->then( + function ($val) { + return $val + 1; + }, + $this->expectCallableNever() + ) + ->then( + $mock, + $this->expectCallableNever() + ); + } + + /** @test */ + public function thenShouldForwardPromisedCallbackResultValueToNextCallback() + { + $adapter = $this->getPromiseTestAdapter(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(2)); + + $adapter->resolve(1); + $adapter->promise() + ->then( + function ($val) { + return \React\Promise\resolve($val + 1); + }, + $this->expectCallableNever() + ) + ->then( + $mock, + $this->expectCallableNever() + ); + } + + /** @test */ + public function thenShouldSwitchFromCallbacksToErrbacksWhenCallbackReturnsARejection() + { + $adapter = $this->getPromiseTestAdapter(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(2)); + + $adapter->resolve(1); + $adapter->promise() + ->then( + function ($val) { + return \React\Promise\reject($val + 1); + }, + $this->expectCallableNever() + ) + ->then( + $this->expectCallableNever(), + $mock + ); + } + + /** @test */ + public function thenShouldSwitchFromCallbacksToErrbacksWhenCallbackThrows() + { + $adapter = $this->getPromiseTestAdapter(); + + $exception = new \Exception(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->will($this->throwException($exception)); + + $mock2 = $this->createCallableMock(); + $mock2 + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo($exception)); + + $adapter->resolve(1); + $adapter->promise() + ->then( + $mock, + $this->expectCallableNever() + ) + ->then( + $this->expectCallableNever(), + $mock2 + ); + } + + /** @test */ + public function cancelShouldReturnNullForFulfilledPromise() + { + $adapter = $this->getPromiseTestAdapter(); + + $adapter->resolve(); + + $this->assertNull($adapter->promise()->cancel()); + } + + /** @test */ + public function cancelShouldHaveNoEffectForFulfilledPromise() + { + $adapter = $this->getPromiseTestAdapter($this->expectCallableNever()); + + $adapter->resolve(); + + $adapter->promise()->cancel(); + } + + /** @test */ + public function doneShouldInvokeFulfillmentHandlerForFulfilledPromise() + { + $adapter = $this->getPromiseTestAdapter(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(1)); + + $adapter->resolve(1); + $this->assertNull($adapter->promise()->done($mock)); + } + + /** @test */ + public function doneShouldThrowExceptionThrownFulfillmentHandlerForFulfilledPromise() + { + $adapter = $this->getPromiseTestAdapter(); + + $this->setExpectedException('\Exception', 'UnhandledRejectionException'); + + $adapter->resolve(1); + $this->assertNull($adapter->promise()->done(function () { + throw new \Exception('UnhandledRejectionException'); + })); + } + + /** @test */ + public function doneShouldThrowUnhandledRejectionExceptionWhenFulfillmentHandlerRejectsForFulfilledPromise() + { + $adapter = $this->getPromiseTestAdapter(); + + $this->setExpectedException('React\\Promise\\UnhandledRejectionException'); + + $adapter->resolve(1); + $this->assertNull($adapter->promise()->done(function () { + return \React\Promise\reject(); + })); + } + + /** @test */ + public function otherwiseShouldNotInvokeRejectionHandlerForFulfilledPromise() + { + $adapter = $this->getPromiseTestAdapter(); + + $adapter->resolve(1); + $adapter->promise()->otherwise($this->expectCallableNever()); + } + + /** @test */ + public function alwaysShouldNotSuppressValueForFulfilledPromise() + { + $adapter = $this->getPromiseTestAdapter(); + + $value = new \stdClass(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo($value)); + + $adapter->resolve($value); + $adapter->promise() + ->always(function () {}) + ->then($mock); + } + + /** @test */ + public function alwaysShouldNotSuppressValueWhenHandlerReturnsANonPromiseForFulfilledPromise() + { + $adapter = $this->getPromiseTestAdapter(); + + $value = new \stdClass(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo($value)); + + $adapter->resolve($value); + $adapter->promise() + ->always(function () { + return 1; + }) + ->then($mock); + } + + /** @test */ + public function alwaysShouldNotSuppressValueWhenHandlerReturnsAPromiseForFulfilledPromise() + { + $adapter = $this->getPromiseTestAdapter(); + + $value = new \stdClass(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo($value)); + + $adapter->resolve($value); + $adapter->promise() + ->always(function () { + return \React\Promise\resolve(1); + }) + ->then($mock); + } + + /** @test */ + public function alwaysShouldRejectWhenHandlerThrowsForFulfilledPromise() + { + $adapter = $this->getPromiseTestAdapter(); + + $exception = new \Exception(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo($exception)); + + $adapter->resolve(1); + $adapter->promise() + ->always(function () use ($exception) { + throw $exception; + }) + ->then(null, $mock); + } + + /** @test */ + public function alwaysShouldRejectWhenHandlerRejectsForFulfilledPromise() + { + $adapter = $this->getPromiseTestAdapter(); + + $exception = new \Exception(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo($exception)); + + $adapter->resolve(1); + $adapter->promise() + ->always(function () use ($exception) { + return \React\Promise\reject($exception); + }) + ->then(null, $mock); + } +} diff --git a/assets/php/vendor/react/promise/tests/PromiseTest/PromisePendingTestTrait.php b/assets/php/vendor/react/promise/tests/PromiseTest/PromisePendingTestTrait.php new file mode 100644 index 0000000..a4f48ee --- /dev/null +++ b/assets/php/vendor/react/promise/tests/PromiseTest/PromisePendingTestTrait.php @@ -0,0 +1,68 @@ +<?php + +namespace React\Promise\PromiseTest; + +trait PromisePendingTestTrait +{ + /** + * @return \React\Promise\PromiseAdapter\PromiseAdapterInterface + */ + abstract public function getPromiseTestAdapter(callable $canceller = null); + + /** @test */ + public function thenShouldReturnAPromiseForPendingPromise() + { + $adapter = $this->getPromiseTestAdapter(); + + $this->assertInstanceOf('React\\Promise\\PromiseInterface', $adapter->promise()->then()); + } + + /** @test */ + public function thenShouldReturnAllowNullForPendingPromise() + { + $adapter = $this->getPromiseTestAdapter(); + + $this->assertInstanceOf('React\\Promise\\PromiseInterface', $adapter->promise()->then(null, null, null)); + } + + /** @test */ + public function cancelShouldReturnNullForPendingPromise() + { + $adapter = $this->getPromiseTestAdapter(); + + $this->assertNull($adapter->promise()->cancel()); + } + + /** @test */ + public function doneShouldReturnNullForPendingPromise() + { + $adapter = $this->getPromiseTestAdapter(); + + $this->assertNull($adapter->promise()->done()); + } + + /** @test */ + public function doneShouldReturnAllowNullForPendingPromise() + { + $adapter = $this->getPromiseTestAdapter(); + + $this->assertNull($adapter->promise()->done(null, null, null)); + } + + /** @test */ + public function otherwiseShouldNotInvokeRejectionHandlerForPendingPromise() + { + $adapter = $this->getPromiseTestAdapter(); + + $adapter->settle(); + $adapter->promise()->otherwise($this->expectCallableNever()); + } + + /** @test */ + public function alwaysShouldReturnAPromiseForPendingPromise() + { + $adapter = $this->getPromiseTestAdapter(); + + $this->assertInstanceOf('React\\Promise\\PromiseInterface', $adapter->promise()->always(function () {})); + } +} diff --git a/assets/php/vendor/react/promise/tests/PromiseTest/PromiseRejectedTestTrait.php b/assets/php/vendor/react/promise/tests/PromiseTest/PromiseRejectedTestTrait.php new file mode 100644 index 0000000..98d1dcf --- /dev/null +++ b/assets/php/vendor/react/promise/tests/PromiseTest/PromiseRejectedTestTrait.php @@ -0,0 +1,512 @@ +<?php + +namespace React\Promise\PromiseTest; + +use React\Promise\Deferred; +use React\Promise\UnhandledRejectionException; + +trait PromiseRejectedTestTrait +{ + /** + * @return \React\Promise\PromiseAdapter\PromiseAdapterInterface + */ + abstract public function getPromiseTestAdapter(callable $canceller = null); + + /** @test */ + public function rejectedPromiseShouldBeImmutable() + { + $adapter = $this->getPromiseTestAdapter(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(1)); + + $adapter->reject(1); + $adapter->reject(2); + + $adapter->promise() + ->then( + $this->expectCallableNever(), + $mock + ); + } + + /** @test */ + public function rejectedPromiseShouldInvokeNewlyAddedCallback() + { + $adapter = $this->getPromiseTestAdapter(); + + $adapter->reject(1); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(1)); + + $adapter->promise() + ->then($this->expectCallableNever(), $mock); + } + + /** @test */ + public function shouldForwardUndefinedRejectionValue() + { + $adapter = $this->getPromiseTestAdapter(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with(null); + + $adapter->reject(1); + $adapter->promise() + ->then( + $this->expectCallableNever(), + function () { + // Presence of rejection handler is enough to switch back + // to resolve mode, even though it returns undefined. + // The ONLY way to propagate a rejection is to re-throw or + // return a rejected promise; + } + ) + ->then( + $mock, + $this->expectCallableNever() + ); + } + + /** @test */ + public function shouldSwitchFromErrbacksToCallbacksWhenErrbackDoesNotExplicitlyPropagate() + { + $adapter = $this->getPromiseTestAdapter(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(2)); + + $adapter->reject(1); + $adapter->promise() + ->then( + $this->expectCallableNever(), + function ($val) { + return $val + 1; + } + ) + ->then( + $mock, + $this->expectCallableNever() + ); + } + + /** @test */ + public function shouldSwitchFromErrbacksToCallbacksWhenErrbackReturnsAResolution() + { + $adapter = $this->getPromiseTestAdapter(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(2)); + + $adapter->reject(1); + $adapter->promise() + ->then( + $this->expectCallableNever(), + function ($val) { + return \React\Promise\resolve($val + 1); + } + ) + ->then( + $mock, + $this->expectCallableNever() + ); + } + + /** @test */ + public function shouldPropagateRejectionsWhenErrbackThrows() + { + $adapter = $this->getPromiseTestAdapter(); + + $exception = new \Exception(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->will($this->throwException($exception)); + + $mock2 = $this->createCallableMock(); + $mock2 + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo($exception)); + + $adapter->reject(1); + $adapter->promise() + ->then( + $this->expectCallableNever(), + $mock + ) + ->then( + $this->expectCallableNever(), + $mock2 + ); + } + + /** @test */ + public function shouldPropagateRejectionsWhenErrbackReturnsARejection() + { + $adapter = $this->getPromiseTestAdapter(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(2)); + + $adapter->reject(1); + $adapter->promise() + ->then( + $this->expectCallableNever(), + function ($val) { + return \React\Promise\reject($val + 1); + } + ) + ->then( + $this->expectCallableNever(), + $mock + ); + } + + /** @test */ + public function doneShouldInvokeRejectionHandlerForRejectedPromise() + { + $adapter = $this->getPromiseTestAdapter(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(1)); + + $adapter->reject(1); + $this->assertNull($adapter->promise()->done(null, $mock)); + } + + /** @test */ + public function doneShouldThrowExceptionThrownByRejectionHandlerForRejectedPromise() + { + $adapter = $this->getPromiseTestAdapter(); + + $this->setExpectedException('\Exception', 'UnhandledRejectionException'); + + $adapter->reject(1); + $this->assertNull($adapter->promise()->done(null, function () { + throw new \Exception('UnhandledRejectionException'); + })); + } + + /** @test */ + public function doneShouldThrowUnhandledRejectionExceptionWhenRejectedWithNonExceptionForRejectedPromise() + { + $adapter = $this->getPromiseTestAdapter(); + + $this->setExpectedException('React\\Promise\\UnhandledRejectionException'); + + $adapter->reject(1); + $this->assertNull($adapter->promise()->done()); + } + + /** @test */ + public function unhandledRejectionExceptionThrownByDoneHoldsRejectionValue() + { + $adapter = $this->getPromiseTestAdapter(); + + $expected = new \stdClass(); + + $adapter->reject($expected); + + try { + $adapter->promise()->done(); + } catch (UnhandledRejectionException $e) { + $this->assertSame($expected, $e->getReason()); + return; + } + + $this->fail(); + } + + /** @test */ + public function doneShouldThrowUnhandledRejectionExceptionWhenRejectionHandlerRejectsForRejectedPromise() + { + $adapter = $this->getPromiseTestAdapter(); + + $this->setExpectedException('React\\Promise\\UnhandledRejectionException'); + + $adapter->reject(1); + $this->assertNull($adapter->promise()->done(null, function () { + return \React\Promise\reject(); + })); + } + + /** @test */ + public function doneShouldThrowRejectionExceptionWhenRejectionHandlerRejectsWithExceptionForRejectedPromise() + { + $adapter = $this->getPromiseTestAdapter(); + + $this->setExpectedException('\Exception', 'UnhandledRejectionException'); + + $adapter->reject(1); + $this->assertNull($adapter->promise()->done(null, function () { + return \React\Promise\reject(new \Exception('UnhandledRejectionException')); + })); + } + + /** @test */ + public function doneShouldThrowExceptionProvidedAsRejectionValueForRejectedPromise() + { + $adapter = $this->getPromiseTestAdapter(); + + $this->setExpectedException('\Exception', 'UnhandledRejectionException'); + + $adapter->reject(new \Exception('UnhandledRejectionException')); + $this->assertNull($adapter->promise()->done()); + } + + /** @test */ + public function doneShouldThrowWithDeepNestingPromiseChainsForRejectedPromise() + { + $this->setExpectedException('\Exception', 'UnhandledRejectionException'); + + $exception = new \Exception('UnhandledRejectionException'); + + $d = new Deferred(); + $d->resolve(); + + $result = \React\Promise\resolve(\React\Promise\resolve($d->promise()->then(function () use ($exception) { + $d = new Deferred(); + $d->resolve(); + + return \React\Promise\resolve($d->promise()->then(function () {}))->then( + function () use ($exception) { + throw $exception; + } + ); + }))); + + $result->done(); + } + + /** @test */ + public function doneShouldRecoverWhenRejectionHandlerCatchesExceptionForRejectedPromise() + { + $adapter = $this->getPromiseTestAdapter(); + + $adapter->reject(new \Exception('UnhandledRejectionException')); + $this->assertNull($adapter->promise()->done(null, function (\Exception $e) { + + })); + } + + /** @test */ + public function otherwiseShouldInvokeRejectionHandlerForRejectedPromise() + { + $adapter = $this->getPromiseTestAdapter(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(1)); + + $adapter->reject(1); + $adapter->promise()->otherwise($mock); + } + + /** @test */ + public function otherwiseShouldInvokeNonTypeHintedRejectionHandlerIfReasonIsAnExceptionForRejectedPromise() + { + $adapter = $this->getPromiseTestAdapter(); + + $exception = new \Exception(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo($exception)); + + $adapter->reject($exception); + $adapter->promise() + ->otherwise(function ($reason) use ($mock) { + $mock($reason); + }); + } + + /** @test */ + public function otherwiseShouldInvokeRejectionHandlerIfReasonMatchesTypehintForRejectedPromise() + { + $adapter = $this->getPromiseTestAdapter(); + + $exception = new \InvalidArgumentException(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo($exception)); + + $adapter->reject($exception); + $adapter->promise() + ->otherwise(function (\InvalidArgumentException $reason) use ($mock) { + $mock($reason); + }); + } + + /** @test */ + public function otherwiseShouldNotInvokeRejectionHandlerIfReaonsDoesNotMatchTypehintForRejectedPromise() + { + $adapter = $this->getPromiseTestAdapter(); + + $exception = new \Exception(); + + $mock = $this->expectCallableNever(); + + $adapter->reject($exception); + $adapter->promise() + ->otherwise(function (\InvalidArgumentException $reason) use ($mock) { + $mock($reason); + }); + } + + /** @test */ + public function alwaysShouldNotSuppressRejectionForRejectedPromise() + { + $adapter = $this->getPromiseTestAdapter(); + + $exception = new \Exception(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo($exception)); + + $adapter->reject($exception); + $adapter->promise() + ->always(function () {}) + ->then(null, $mock); + } + + /** @test */ + public function alwaysShouldNotSuppressRejectionWhenHandlerReturnsANonPromiseForRejectedPromise() + { + $adapter = $this->getPromiseTestAdapter(); + + $exception = new \Exception(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo($exception)); + + $adapter->reject($exception); + $adapter->promise() + ->always(function () { + return 1; + }) + ->then(null, $mock); + } + + /** @test */ + public function alwaysShouldNotSuppressRejectionWhenHandlerReturnsAPromiseForRejectedPromise() + { + $adapter = $this->getPromiseTestAdapter(); + + $exception = new \Exception(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo($exception)); + + $adapter->reject($exception); + $adapter->promise() + ->always(function () { + return \React\Promise\resolve(1); + }) + ->then(null, $mock); + } + + /** @test */ + public function alwaysShouldRejectWhenHandlerThrowsForRejectedPromise() + { + $adapter = $this->getPromiseTestAdapter(); + + $exception1 = new \Exception(); + $exception2 = new \Exception(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo($exception2)); + + $adapter->reject($exception1); + $adapter->promise() + ->always(function () use ($exception2) { + throw $exception2; + }) + ->then(null, $mock); + } + + /** @test */ + public function alwaysShouldRejectWhenHandlerRejectsForRejectedPromise() + { + $adapter = $this->getPromiseTestAdapter(); + + $exception1 = new \Exception(); + $exception2 = new \Exception(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo($exception2)); + + $adapter->reject($exception1); + $adapter->promise() + ->always(function () use ($exception2) { + return \React\Promise\reject($exception2); + }) + ->then(null, $mock); + } + + /** @test */ + public function cancelShouldReturnNullForRejectedPromise() + { + $adapter = $this->getPromiseTestAdapter(); + + $adapter->reject(); + + $this->assertNull($adapter->promise()->cancel()); + } + + /** @test */ + public function cancelShouldHaveNoEffectForRejectedPromise() + { + $adapter = $this->getPromiseTestAdapter($this->expectCallableNever()); + + $adapter->reject(); + + $adapter->promise()->cancel(); + } +} diff --git a/assets/php/vendor/react/promise/tests/PromiseTest/PromiseSettledTestTrait.php b/assets/php/vendor/react/promise/tests/PromiseTest/PromiseSettledTestTrait.php new file mode 100644 index 0000000..e363b6d --- /dev/null +++ b/assets/php/vendor/react/promise/tests/PromiseTest/PromiseSettledTestTrait.php @@ -0,0 +1,86 @@ +<?php + +namespace React\Promise\PromiseTest; + +trait PromiseSettledTestTrait +{ + /** + * @return \React\Promise\PromiseAdapter\PromiseAdapterInterface + */ + abstract public function getPromiseTestAdapter(callable $canceller = null); + + /** @test */ + public function thenShouldReturnAPromiseForSettledPromise() + { + $adapter = $this->getPromiseTestAdapter(); + + $adapter->settle(); + $this->assertInstanceOf('React\\Promise\\PromiseInterface', $adapter->promise()->then()); + } + + /** @test */ + public function thenShouldReturnAllowNullForSettledPromise() + { + $adapter = $this->getPromiseTestAdapter(); + + $adapter->settle(); + $this->assertInstanceOf('React\\Promise\\PromiseInterface', $adapter->promise()->then(null, null, null)); + } + + /** @test */ + public function cancelShouldReturnNullForSettledPromise() + { + $adapter = $this->getPromiseTestAdapter(); + + $adapter->settle(); + + $this->assertNull($adapter->promise()->cancel()); + } + + /** @test */ + public function cancelShouldHaveNoEffectForSettledPromise() + { + $adapter = $this->getPromiseTestAdapter($this->expectCallableNever()); + + $adapter->settle(); + + $adapter->promise()->cancel(); + } + + /** @test */ + public function doneShouldReturnNullForSettledPromise() + { + $adapter = $this->getPromiseTestAdapter(); + + $adapter->settle(); + $this->assertNull($adapter->promise()->done(null, function () {})); + } + + /** @test */ + public function doneShouldReturnAllowNullForSettledPromise() + { + $adapter = $this->getPromiseTestAdapter(); + + $adapter->settle(); + $this->assertNull($adapter->promise()->done(null, function () {}, null)); + } + + /** @test */ + public function progressShouldNotInvokeProgressHandlerForSettledPromise() + { + $adapter = $this->getPromiseTestAdapter(); + + $adapter->settle(); + $adapter->promise()->progress($this->expectCallableNever()); + $adapter->notify(); + } + + /** @test */ + public function alwaysShouldReturnAPromiseForSettledPromise() + { + $adapter = $this->getPromiseTestAdapter(); + + $adapter->settle(); + $this->assertInstanceOf('React\\Promise\\PromiseInterface', $adapter->promise()->always(function () {})); + } +} diff --git a/assets/php/vendor/react/promise/tests/PromiseTest/RejectTestTrait.php b/assets/php/vendor/react/promise/tests/PromiseTest/RejectTestTrait.php new file mode 100644 index 0000000..063f178 --- /dev/null +++ b/assets/php/vendor/react/promise/tests/PromiseTest/RejectTestTrait.php @@ -0,0 +1,368 @@ +<?php + +namespace React\Promise\PromiseTest; + +use React\Promise; +use React\Promise\Deferred; + +trait RejectTestTrait +{ + /** + * @return \React\Promise\PromiseAdapter\PromiseAdapterInterface + */ + abstract public function getPromiseTestAdapter(callable $canceller = null); + + /** @test */ + public function rejectShouldRejectWithAnImmediateValue() + { + $adapter = $this->getPromiseTestAdapter(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(1)); + + $adapter->promise() + ->then($this->expectCallableNever(), $mock); + + $adapter->reject(1); + } + + /** @test */ + public function rejectShouldRejectWithFulfilledPromise() + { + $adapter = $this->getPromiseTestAdapter(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(1)); + + $adapter->promise() + ->then($this->expectCallableNever(), $mock); + + $adapter->reject(Promise\resolve(1)); + } + + /** @test */ + public function rejectShouldRejectWithRejectedPromise() + { + $adapter = $this->getPromiseTestAdapter(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(1)); + + $adapter->promise() + ->then($this->expectCallableNever(), $mock); + + $adapter->reject(Promise\reject(1)); + } + + /** @test */ + public function rejectShouldForwardReasonWhenCallbackIsNull() + { + $adapter = $this->getPromiseTestAdapter(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(1)); + + $adapter->promise() + ->then( + $this->expectCallableNever() + ) + ->then( + $this->expectCallableNever(), + $mock + ); + + $adapter->reject(1); + } + + /** @test */ + public function rejectShouldMakePromiseImmutable() + { + $adapter = $this->getPromiseTestAdapter(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(1)); + + $adapter->promise() + ->then(null, function ($value) use ($adapter) { + $adapter->reject(3); + + return Promise\reject($value); + }) + ->then( + $this->expectCallableNever(), + $mock + ); + + $adapter->reject(1); + $adapter->reject(2); + } + + /** @test */ + public function notifyShouldInvokeOtherwiseHandler() + { + $adapter = $this->getPromiseTestAdapter(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(1)); + + $adapter->promise() + ->otherwise($mock); + + $adapter->reject(1); + } + + /** @test */ + public function doneShouldInvokeRejectionHandler() + { + $adapter = $this->getPromiseTestAdapter(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(1)); + + $this->assertNull($adapter->promise()->done(null, $mock)); + $adapter->reject(1); + } + + /** @test */ + public function doneShouldThrowExceptionThrownByRejectionHandler() + { + $adapter = $this->getPromiseTestAdapter(); + + $this->setExpectedException('\Exception', 'UnhandledRejectionException'); + + $this->assertNull($adapter->promise()->done(null, function () { + throw new \Exception('UnhandledRejectionException'); + })); + $adapter->reject(1); + } + + /** @test */ + public function doneShouldThrowUnhandledRejectionExceptionWhenRejectedWithNonException() + { + $adapter = $this->getPromiseTestAdapter(); + + $this->setExpectedException('React\\Promise\\UnhandledRejectionException'); + + $this->assertNull($adapter->promise()->done()); + $adapter->reject(1); + } + + /** @test */ + public function doneShouldThrowUnhandledRejectionExceptionWhenRejectionHandlerRejects() + { + $adapter = $this->getPromiseTestAdapter(); + + $this->setExpectedException('React\\Promise\\UnhandledRejectionException'); + + $this->assertNull($adapter->promise()->done(null, function () { + return \React\Promise\reject(); + })); + $adapter->reject(1); + } + + /** @test */ + public function doneShouldThrowRejectionExceptionWhenRejectionHandlerRejectsWithException() + { + $adapter = $this->getPromiseTestAdapter(); + + $this->setExpectedException('\Exception', 'UnhandledRejectionException'); + + $this->assertNull($adapter->promise()->done(null, function () { + return \React\Promise\reject(new \Exception('UnhandledRejectionException')); + })); + $adapter->reject(1); + } + + /** @test */ + public function doneShouldThrowUnhandledRejectionExceptionWhenRejectionHandlerRetunsPendingPromiseWhichRejectsLater() + { + $adapter = $this->getPromiseTestAdapter(); + + $this->setExpectedException('React\\Promise\\UnhandledRejectionException'); + + $d = new Deferred(); + $promise = $d->promise(); + + $this->assertNull($adapter->promise()->done(null, function () use ($promise) { + return $promise; + })); + $adapter->reject(1); + $d->reject(1); + } + + /** @test */ + public function doneShouldThrowExceptionProvidedAsRejectionValue() + { + $adapter = $this->getPromiseTestAdapter(); + + $this->setExpectedException('\Exception', 'UnhandledRejectionException'); + + $this->assertNull($adapter->promise()->done()); + $adapter->reject(new \Exception('UnhandledRejectionException')); + } + + /** @test */ + public function doneShouldThrowWithDeepNestingPromiseChains() + { + $this->setExpectedException('\Exception', 'UnhandledRejectionException'); + + $exception = new \Exception('UnhandledRejectionException'); + + $d = new Deferred(); + + $result = \React\Promise\resolve(\React\Promise\resolve($d->promise()->then(function () use ($exception) { + $d = new Deferred(); + $d->resolve(); + + return \React\Promise\resolve($d->promise()->then(function () {}))->then( + function () use ($exception) { + throw $exception; + } + ); + }))); + + $result->done(); + + $d->resolve(); + } + + /** @test */ + public function doneShouldRecoverWhenRejectionHandlerCatchesException() + { + $adapter = $this->getPromiseTestAdapter(); + + $this->assertNull($adapter->promise()->done(null, function (\Exception $e) { + + })); + $adapter->reject(new \Exception('UnhandledRejectionException')); + } + + /** @test */ + public function alwaysShouldNotSuppressRejection() + { + $adapter = $this->getPromiseTestAdapter(); + + $exception = new \Exception(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo($exception)); + + $adapter->promise() + ->always(function () {}) + ->then(null, $mock); + + $adapter->reject($exception); + } + + /** @test */ + public function alwaysShouldNotSuppressRejectionWhenHandlerReturnsANonPromise() + { + $adapter = $this->getPromiseTestAdapter(); + + $exception = new \Exception(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo($exception)); + + $adapter->promise() + ->always(function () { + return 1; + }) + ->then(null, $mock); + + $adapter->reject($exception); + } + + /** @test */ + public function alwaysShouldNotSuppressRejectionWhenHandlerReturnsAPromise() + { + $adapter = $this->getPromiseTestAdapter(); + + $exception = new \Exception(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo($exception)); + + $adapter->promise() + ->always(function () { + return \React\Promise\resolve(1); + }) + ->then(null, $mock); + + $adapter->reject($exception); + } + + /** @test */ + public function alwaysShouldRejectWhenHandlerThrowsForRejection() + { + $adapter = $this->getPromiseTestAdapter(); + + $exception = new \Exception(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo($exception)); + + $adapter->promise() + ->always(function () use ($exception) { + throw $exception; + }) + ->then(null, $mock); + + $adapter->reject($exception); + } + + /** @test */ + public function alwaysShouldRejectWhenHandlerRejectsForRejection() + { + $adapter = $this->getPromiseTestAdapter(); + + $exception = new \Exception(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo($exception)); + + $adapter->promise() + ->always(function () use ($exception) { + return \React\Promise\reject($exception); + }) + ->then(null, $mock); + + $adapter->reject($exception); + } +} diff --git a/assets/php/vendor/react/promise/tests/PromiseTest/ResolveTestTrait.php b/assets/php/vendor/react/promise/tests/PromiseTest/ResolveTestTrait.php new file mode 100644 index 0000000..0736d35 --- /dev/null +++ b/assets/php/vendor/react/promise/tests/PromiseTest/ResolveTestTrait.php @@ -0,0 +1,312 @@ +<?php + +namespace React\Promise\PromiseTest; + +use React\Promise; + +trait ResolveTestTrait +{ + /** + * @return \React\Promise\PromiseAdapter\PromiseAdapterInterface + */ + abstract public function getPromiseTestAdapter(callable $canceller = null); + + /** @test */ + public function resolveShouldResolve() + { + $adapter = $this->getPromiseTestAdapter(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(1)); + + $adapter->promise() + ->then($mock); + + $adapter->resolve(1); + } + + /** @test */ + public function resolveShouldResolveWithPromisedValue() + { + $adapter = $this->getPromiseTestAdapter(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(1)); + + $adapter->promise() + ->then($mock); + + $adapter->resolve(Promise\resolve(1)); + } + + /** @test */ + public function resolveShouldRejectWhenResolvedWithRejectedPromise() + { + $adapter = $this->getPromiseTestAdapter(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(1)); + + $adapter->promise() + ->then($this->expectCallableNever(), $mock); + + $adapter->resolve(Promise\reject(1)); + } + + /** @test */ + public function resolveShouldForwardValueWhenCallbackIsNull() + { + $adapter = $this->getPromiseTestAdapter(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(1)); + + $adapter->promise() + ->then( + null, + $this->expectCallableNever() + ) + ->then( + $mock, + $this->expectCallableNever() + ); + + $adapter->resolve(1); + } + + /** @test */ + public function resolveShouldMakePromiseImmutable() + { + $adapter = $this->getPromiseTestAdapter(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(1)); + + $adapter->promise() + ->then(function ($value) use ($adapter) { + $adapter->resolve(3); + + return $value; + }) + ->then( + $mock, + $this->expectCallableNever() + ); + + $adapter->resolve(1); + $adapter->resolve(2); + } + + /** + * @test + */ + public function resolveShouldRejectWhenResolvedWithItself() + { + $adapter = $this->getPromiseTestAdapter(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with(new \LogicException('Cannot resolve a promise with itself.')); + + $adapter->promise() + ->then( + $this->expectCallableNever(), + $mock + ); + + $adapter->resolve($adapter->promise()); + } + + /** + * @test + */ + public function resolveShouldRejectWhenResolvedWithAPromiseWhichFollowsItself() + { + $adapter1 = $this->getPromiseTestAdapter(); + $adapter2 = $this->getPromiseTestAdapter(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with(new \LogicException('Cannot resolve a promise with itself.')); + + $promise1 = $adapter1->promise(); + + $promise2 = $adapter2->promise(); + + $promise2->then( + $this->expectCallableNever(), + $mock + ); + + $adapter1->resolve($promise2); + $adapter2->resolve($promise1); + } + + /** @test */ + public function doneShouldInvokeFulfillmentHandler() + { + $adapter = $this->getPromiseTestAdapter(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(1)); + + $this->assertNull($adapter->promise()->done($mock)); + $adapter->resolve(1); + } + + /** @test */ + public function doneShouldThrowExceptionThrownFulfillmentHandler() + { + $adapter = $this->getPromiseTestAdapter(); + + $this->setExpectedException('\Exception', 'UnhandledRejectionException'); + + $this->assertNull($adapter->promise()->done(function () { + throw new \Exception('UnhandledRejectionException'); + })); + $adapter->resolve(1); + } + + /** @test */ + public function doneShouldThrowUnhandledRejectionExceptionWhenFulfillmentHandlerRejects() + { + $adapter = $this->getPromiseTestAdapter(); + + $this->setExpectedException('React\\Promise\\UnhandledRejectionException'); + + $this->assertNull($adapter->promise()->done(function () { + return \React\Promise\reject(); + })); + $adapter->resolve(1); + } + + /** @test */ + public function alwaysShouldNotSuppressValue() + { + $adapter = $this->getPromiseTestAdapter(); + + $value = new \stdClass(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo($value)); + + $adapter->promise() + ->always(function () {}) + ->then($mock); + + $adapter->resolve($value); + } + + /** @test */ + public function alwaysShouldNotSuppressValueWhenHandlerReturnsANonPromise() + { + $adapter = $this->getPromiseTestAdapter(); + + $value = new \stdClass(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo($value)); + + $adapter->promise() + ->always(function () { + return 1; + }) + ->then($mock); + + $adapter->resolve($value); + } + + /** @test */ + public function alwaysShouldNotSuppressValueWhenHandlerReturnsAPromise() + { + $adapter = $this->getPromiseTestAdapter(); + + $value = new \stdClass(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo($value)); + + $adapter->promise() + ->always(function () { + return \React\Promise\resolve(1); + }) + ->then($mock); + + $adapter->resolve($value); + } + + /** @test */ + public function alwaysShouldRejectWhenHandlerThrowsForFulfillment() + { + $adapter = $this->getPromiseTestAdapter(); + + $exception = new \Exception(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo($exception)); + + $adapter->promise() + ->always(function () use ($exception) { + throw $exception; + }) + ->then(null, $mock); + + $adapter->resolve(1); + } + + /** @test */ + public function alwaysShouldRejectWhenHandlerRejectsForFulfillment() + { + $adapter = $this->getPromiseTestAdapter(); + + $exception = new \Exception(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo($exception)); + + $adapter->promise() + ->always(function () use ($exception) { + return \React\Promise\reject($exception); + }) + ->then(null, $mock); + + $adapter->resolve(1); + } +} diff --git a/assets/php/vendor/react/promise/tests/RejectedPromiseTest.php b/assets/php/vendor/react/promise/tests/RejectedPromiseTest.php new file mode 100644 index 0000000..c886b00 --- /dev/null +++ b/assets/php/vendor/react/promise/tests/RejectedPromiseTest.php @@ -0,0 +1,50 @@ +<?php + +namespace React\Promise; + +use React\Promise\PromiseAdapter\CallbackPromiseAdapter; + +class RejectedPromiseTest extends TestCase +{ + use PromiseTest\PromiseSettledTestTrait, + PromiseTest\PromiseRejectedTestTrait; + + public function getPromiseTestAdapter(callable $canceller = null) + { + $promise = null; + + return new CallbackPromiseAdapter([ + 'promise' => function () use (&$promise) { + if (!$promise) { + throw new \LogicException('RejectedPromise must be rejected before obtaining the promise'); + } + + return $promise; + }, + 'resolve' => function () { + throw new \LogicException('You cannot call resolve() for React\Promise\RejectedPromise'); + }, + 'reject' => function ($reason = null) use (&$promise) { + if (!$promise) { + $promise = new RejectedPromise($reason); + } + }, + 'notify' => function () { + // no-op + }, + 'settle' => function ($reason = null) use (&$promise) { + if (!$promise) { + $promise = new RejectedPromise($reason); + } + }, + ]); + } + + /** @test */ + public function shouldThrowExceptionIfConstructedWithAPromise() + { + $this->setExpectedException('\InvalidArgumentException'); + + return new RejectedPromise(new RejectedPromise()); + } +} diff --git a/assets/php/vendor/react/promise/tests/Stub/CallableStub.php b/assets/php/vendor/react/promise/tests/Stub/CallableStub.php new file mode 100644 index 0000000..0120893 --- /dev/null +++ b/assets/php/vendor/react/promise/tests/Stub/CallableStub.php @@ -0,0 +1,10 @@ +<?php + +namespace React\Promise\Stub; + +class CallableStub +{ + public function __invoke() + { + } +} diff --git a/assets/php/vendor/react/promise/tests/TestCase.php b/assets/php/vendor/react/promise/tests/TestCase.php new file mode 100644 index 0000000..c9274f4 --- /dev/null +++ b/assets/php/vendor/react/promise/tests/TestCase.php @@ -0,0 +1,43 @@ +<?php + +namespace React\Promise; + +class TestCase extends \PHPUnit_Framework_TestCase +{ + public function expectCallableExactly($amount) + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->exactly($amount)) + ->method('__invoke'); + + return $mock; + } + + public function expectCallableOnce() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke'); + + return $mock; + } + + public function expectCallableNever() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->never()) + ->method('__invoke'); + + return $mock; + } + + public function createCallableMock() + { + return $this + ->getMockBuilder('React\\Promise\Stub\CallableStub') + ->getMock(); + } +} diff --git a/assets/php/vendor/react/promise/tests/bootstrap.php b/assets/php/vendor/react/promise/tests/bootstrap.php new file mode 100644 index 0000000..9b7f872 --- /dev/null +++ b/assets/php/vendor/react/promise/tests/bootstrap.php @@ -0,0 +1,7 @@ +<?php + +$loader = @include __DIR__.'/../vendor/autoload.php'; +if (!$loader) { + $loader = require __DIR__.'/../../../../vendor/autoload.php'; +} +$loader->addPsr4('React\\Promise\\', __DIR__); diff --git a/assets/php/vendor/react/promise/tests/fixtures/SimpleFulfilledTestPromise.php b/assets/php/vendor/react/promise/tests/fixtures/SimpleFulfilledTestPromise.php new file mode 100644 index 0000000..ef4d530 --- /dev/null +++ b/assets/php/vendor/react/promise/tests/fixtures/SimpleFulfilledTestPromise.php @@ -0,0 +1,21 @@ +<?php + +namespace React\Promise; + +class SimpleFulfilledTestPromise implements PromiseInterface +{ + public function then(callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null) + { + try { + if ($onFulfilled) { + $onFulfilled('foo'); + } + + return new self(); + } catch (\Throwable $exception) { + return new RejectedPromise($exception); + } catch (\Exception $exception) { + return new RejectedPromise($exception); + } + } +} diff --git a/assets/php/vendor/react/promise/tests/fixtures/SimpleFulfilledTestThenable.php b/assets/php/vendor/react/promise/tests/fixtures/SimpleFulfilledTestThenable.php new file mode 100644 index 0000000..3f66f63 --- /dev/null +++ b/assets/php/vendor/react/promise/tests/fixtures/SimpleFulfilledTestThenable.php @@ -0,0 +1,21 @@ +<?php + +namespace React\Promise; + +class SimpleFulfilledTestThenable +{ + public function then(callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null) + { + try { + if ($onFulfilled) { + $onFulfilled('foo'); + } + + return new self(); + } catch (\Throwable $exception) { + return new RejectedPromise($exception); + } catch (\Exception $exception) { + return new RejectedPromise($exception); + } + } +} diff --git a/assets/php/vendor/react/promise/tests/fixtures/SimpleRejectedTestPromise.php b/assets/php/vendor/react/promise/tests/fixtures/SimpleRejectedTestPromise.php new file mode 100644 index 0000000..b30a226 --- /dev/null +++ b/assets/php/vendor/react/promise/tests/fixtures/SimpleRejectedTestPromise.php @@ -0,0 +1,21 @@ +<?php + +namespace React\Promise; + +class SimpleRejectedTestPromise implements PromiseInterface +{ + public function then(callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null) + { + try { + if ($onRejected) { + $onRejected('foo'); + } + + return new self(); + } catch (\Throwable $exception) { + return new RejectedPromise($exception); + } catch (\Exception $exception) { + return new RejectedPromise($exception); + } + } +} diff --git a/assets/php/vendor/react/promise/tests/fixtures/SimpleTestCancellable.php b/assets/php/vendor/react/promise/tests/fixtures/SimpleTestCancellable.php new file mode 100644 index 0000000..f232a68 --- /dev/null +++ b/assets/php/vendor/react/promise/tests/fixtures/SimpleTestCancellable.php @@ -0,0 +1,13 @@ +<?php + +namespace React\Promise; + +class SimpleTestCancellable +{ + public $cancelCalled = false; + + public function cancel() + { + $this->cancelCalled = true; + } +} diff --git a/assets/php/vendor/react/promise/tests/fixtures/SimpleTestCancellableThenable.php b/assets/php/vendor/react/promise/tests/fixtures/SimpleTestCancellableThenable.php new file mode 100644 index 0000000..c0f1593 --- /dev/null +++ b/assets/php/vendor/react/promise/tests/fixtures/SimpleTestCancellableThenable.php @@ -0,0 +1,18 @@ +<?php + +namespace React\Promise; + +class SimpleTestCancellableThenable +{ + public $cancelCalled = false; + + public function then(callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null) + { + return new self(); + } + + public function cancel() + { + $this->cancelCalled = true; + } +} diff --git a/assets/php/vendor/react/socket/.gitignore b/assets/php/vendor/react/socket/.gitignore new file mode 100644 index 0000000..987e2a2 --- /dev/null +++ b/assets/php/vendor/react/socket/.gitignore @@ -0,0 +1,2 @@ +composer.lock +vendor diff --git a/assets/php/vendor/react/socket/.travis.yml b/assets/php/vendor/react/socket/.travis.yml new file mode 100644 index 0000000..917dc0c --- /dev/null +++ b/assets/php/vendor/react/socket/.travis.yml @@ -0,0 +1,49 @@ +language: php + +php: +# - 5.3 # requires old distro, see below + - 5.4 + - 5.5 + - 5.6 + - 7.0 + - 7.1 + - 7.2 +# - 7.0 # Mac OS X, ignore errors, see below + - hhvm # ignore errors, see below + +# lock distro so new future defaults will not break the build +dist: trusty + +matrix: + include: + - php: 5.3 + dist: precise + include: + - os: osx + language: generic + php: 7.0 # just to look right on travis + env: + - PACKAGE: php70 + allow_failures: + - php: hhvm + - os: osx + +sudo: false + +install: + # OSX install inspired by https://github.com/kiler129/TravisCI-OSX-PHP + - | + if [[ "${TRAVIS_OS_NAME}" == "osx" ]]; then + brew tap homebrew/homebrew-php + echo "Installing PHP ..." + brew install "${PACKAGE}" + brew install "${PACKAGE}"-xdebug + brew link "${PACKAGE}" + echo "Installing composer ..." + curl -s http://getcomposer.org/installer | php + mv composer.phar /usr/local/bin/composer + fi + - composer install --no-interaction + +script: + - ./vendor/bin/phpunit --coverage-text diff --git a/assets/php/vendor/react/socket/CHANGELOG.md b/assets/php/vendor/react/socket/CHANGELOG.md new file mode 100644 index 0000000..03c2eec --- /dev/null +++ b/assets/php/vendor/react/socket/CHANGELOG.md @@ -0,0 +1,451 @@ +# Changelog + +## 0.8.10 (2018-02-28) + +* Feature: Update DNS dependency to support loading system default DNS + nameserver config on all supported platforms + (`/etc/resolv.conf` on Unix/Linux/Mac/Docker/WSL and WMIC on Windows) + (#152 by @clue) + + This means that connecting to hosts that are managed by a local DNS server, + such as a corporate DNS server or when using Docker containers, will now + work as expected across all platforms with no changes required: + + ```php + $connector = new Connector($loop); + $connector->connect('intranet.example:80')->then(function ($connection) { + // … + }); + ``` + +## 0.8.9 (2018-01-18) + +* Feature: Support explicitly choosing TLS version to negotiate with remote side + by respecting `crypto_method` context parameter for all classes. + (#149 by @clue) + + By default, all connector and server classes support TLSv1.0+ and exclude + support for legacy SSLv2/SSLv3. As of PHP 5.6+ you can also explicitly + choose the TLS version you want to negotiate with the remote side: + + ```php + // new: now supports 'crypto_method` context parameter for all classes + $connector = new Connector($loop, array( + 'tls' => array( + 'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT + ) + )); + ``` + +* Minor internal clean up to unify class imports + (#148 by @clue) + +## 0.8.8 (2018-01-06) + +* Improve test suite by adding test group to skip integration tests relying on + internet connection and fix minor documentation typo. + (#146 by @clue and #145 by @cn007b) + +## 0.8.7 (2017-12-24) + +* Fix: Fix closing socket resource before removing from loop + (#141 by @clue) + + This fixes the root cause of an uncaught `Exception` that only manifested + itself after the recent Stream v0.7.4 component update and only if you're + using `ext-event` (`ExtEventLoop`). + +* Improve test suite by testing against PHP 7.2 + (#140 by @carusogabriel) + +## 0.8.6 (2017-11-18) + +* Feature: Add Unix domain socket (UDS) support to `Server` with `unix://` URI scheme + and add advanced `UnixServer` class. + (#120 by @andig) + + ```php + // new: Server now supports "unix://" scheme + $server = new Server('unix:///tmp/server.sock', $loop); + + // new: advanced usage + $server = new UnixServer('/tmp/server.sock', $loop); + ``` + +* Restructure examples to ease getting started + (#136 by @clue) + +* Improve test suite by adding forward compatibility with PHPUnit 6 and + ignore Mac OS X test failures for now until Travis tests work again + (#133 by @gabriel-caruso and #134 by @clue) + +## 0.8.5 (2017-10-23) + +* Fix: Work around PHP bug with Unix domain socket (UDS) paths for Mac OS X + (#123 by @andig) + +* Fix: Fix `SecureServer` to return `null` URI if server socket is already closed + (#129 by @clue) + +* Improve test suite by adding forward compatibility with PHPUnit v5 and + forward compatibility with upcoming EventLoop releases in tests and + test Mac OS X on Travis + (#122 by @andig and #125, #127 and #130 by @clue) + +* Readme improvements + (#118 by @jsor) + +## 0.8.4 (2017-09-16) + +* Feature: Add `FixedUriConnector` decorator to use fixed, preconfigured URI instead + (#117 by @clue) + + This can be useful for consumers that do not support certain URIs, such as + when you want to explicitly connect to a Unix domain socket (UDS) path + instead of connecting to a default address assumed by an higher-level API: + + ```php + $connector = new FixedUriConnector( + 'unix:///var/run/docker.sock', + new UnixConnector($loop) + ); + + // destination will be ignored, actually connects to Unix domain socket + $promise = $connector->connect('localhost:80'); + ``` + +## 0.8.3 (2017-09-08) + +* Feature: Reduce memory consumption for failed connections + (#113 by @valga) + +* Fix: Work around write chunk size for TLS streams for PHP < 7.1.14 + (#114 by @clue) + +## 0.8.2 (2017-08-25) + +* Feature: Update DNS dependency to support hosts file on all platforms + (#112 by @clue) + + This means that connecting to hosts such as `localhost` will now work as + expected across all platforms with no changes required: + + ```php + $connector = new Connector($loop); + $connector->connect('localhost:8080')->then(function ($connection) { + // … + }); + ``` + +## 0.8.1 (2017-08-15) + +* Feature: Forward compatibility with upcoming EventLoop v1.0 and v0.5 and + target evenement 3.0 a long side 2.0 and 1.0 + (#104 by @clue and #111 by @WyriHaximus) + +* Improve test suite by locking Travis distro so new defaults will not break the build and + fix HHVM build for now again and ignore future HHVM build errors + (#109 and #110 by @clue) + +* Minor documentation fixes + (#103 by @christiaan and #108 by @hansott) + +## 0.8.0 (2017-05-09) + +* Feature: New `Server` class now acts as a facade for existing server classes + and renamed old `Server` to `TcpServer` for advanced usage. + (#96 and #97 by @clue) + + The `Server` class is now the main class in this package that implements the + `ServerInterface` and allows you to accept incoming streaming connections, + such as plaintext TCP/IP or secure TLS connection streams. + + > This is not a BC break and consumer code does not have to be updated. + +* Feature / BC break: All addresses are now URIs that include the URI scheme + (#98 by @clue) + + ```diff + - $parts = parse_url('tcp://' . $conn->getRemoteAddress()); + + $parts = parse_url($conn->getRemoteAddress()); + ``` + +* Fix: Fix `unix://` addresses for Unix domain socket (UDS) paths + (#100 by @clue) + +* Feature: Forward compatibility with Stream v1.0 and v0.7 + (#99 by @clue) + +## 0.7.2 (2017-04-24) + +* Fix: Work around latest PHP 7.0.18 and 7.1.4 no longer accepting full URIs + (#94 by @clue) + +## 0.7.1 (2017-04-10) + +* Fix: Ignore HHVM errors when closing connection that is already closing + (#91 by @clue) + +## 0.7.0 (2017-04-10) + +* Feature: Merge SocketClient component into this component + (#87 by @clue) + + This means that this package now provides async, streaming plaintext TCP/IP + and secure TLS socket server and client connections for ReactPHP. + + ``` + $connector = new React\Socket\Connector($loop); + $connector->connect('google.com:80')->then(function (ConnectionInterface $conn) { + $connection->write('…'); + }); + ``` + + Accordingly, the `ConnectionInterface` is now used to represent both incoming + server side connections as well as outgoing client side connections. + + If you've previously used the SocketClient component to establish outgoing + client connections, upgrading should take no longer than a few minutes. + All classes have been merged as-is from the latest `v0.7.0` release with no + other changes, so you can simply update your code to use the updated namespace + like this: + + ```php + // old from SocketClient component and namespace + $connector = new React\SocketClient\Connector($loop); + $connector->connect('google.com:80')->then(function (ConnectionInterface $conn) { + $connection->write('…'); + }); + + // new + $connector = new React\Socket\Connector($loop); + $connector->connect('google.com:80')->then(function (ConnectionInterface $conn) { + $connection->write('…'); + }); + ``` + +## 0.6.0 (2017-04-04) + +* Feature: Add `LimitingServer` to limit and keep track of open connections + (#86 by @clue) + + ```php + $server = new Server(0, $loop); + $server = new LimitingServer($server, 100); + + $server->on('connection', function (ConnectionInterface $connection) { + $connection->write('hello there!' . PHP_EOL); + … + }); + ``` + +* Feature / BC break: Add `pause()` and `resume()` methods to limit active + connections + (#84 by @clue) + + ```php + $server = new Server(0, $loop); + $server->pause(); + + $loop->addTimer(1.0, function() use ($server) { + $server->resume(); + }); + ``` + +## 0.5.1 (2017-03-09) + +* Feature: Forward compatibility with Stream v0.5 and upcoming v0.6 + (#79 by @clue) + +## 0.5.0 (2017-02-14) + +* Feature / BC break: Replace `listen()` call with URIs passed to constructor + and reject listening on hostnames with `InvalidArgumentException` + and replace `ConnectionException` with `RuntimeException` for consistency + (#61, #66 and #72 by @clue) + + ```php + // old + $server = new Server($loop); + $server->listen(8080); + + // new + $server = new Server(8080, $loop); + ``` + + Similarly, you can now pass a full listening URI to the constructor to change + the listening host: + + ```php + // old + $server = new Server($loop); + $server->listen(8080, '127.0.0.1'); + + // new + $server = new Server('127.0.0.1:8080', $loop); + ``` + + Trying to start listening on (DNS) host names will now throw an + `InvalidArgumentException`, use IP addresses instead: + + ```php + // old + $server = new Server($loop); + $server->listen(8080, 'localhost'); + + // new + $server = new Server('127.0.0.1:8080', $loop); + ``` + + If trying to listen fails (such as if port is already in use or port below + 1024 may require root access etc.), it will now throw a `RuntimeException`, + the `ConnectionException` class has been removed: + + ```php + // old: throws React\Socket\ConnectionException + $server = new Server($loop); + $server->listen(80); + + // new: throws RuntimeException + $server = new Server(80, $loop); + ``` + +* Feature / BC break: Rename `shutdown()` to `close()` for consistency throughout React + (#62 by @clue) + + ```php + // old + $server->shutdown(); + + // new + $server->close(); + ``` + +* Feature / BC break: Replace `getPort()` with `getAddress()` + (#67 by @clue) + + ```php + // old + echo $server->getPort(); // 8080 + + // new + echo $server->getAddress(); // 127.0.0.1:8080 + ``` + +* Feature / BC break: `getRemoteAddress()` returns full address instead of only IP + (#65 by @clue) + + ```php + // old + echo $connection->getRemoteAddress(); // 192.168.0.1 + + // new + echo $connection->getRemoteAddress(); // 192.168.0.1:51743 + ``` + +* Feature / BC break: Add `getLocalAddress()` method + (#68 by @clue) + + ```php + echo $connection->getLocalAddress(); // 127.0.0.1:8080 + ``` + +* BC break: The `Server` and `SecureServer` class are now marked `final` + and you can no longer `extend` them + (which was never documented or recommended anyway). + Public properties and event handlers are now internal only. + Please use composition instead of extension. + (#71, #70 and #69 by @clue) + +## 0.4.6 (2017-01-26) + +* Feature: Support socket context options passed to `Server` + (#64 by @clue) + +* Fix: Properly return `null` for unknown addresses + (#63 by @clue) + +* Improve documentation for `ServerInterface` and lock test suite requirements + (#60 by @clue, #57 by @shaunbramley) + +## 0.4.5 (2017-01-08) + +* Feature: Add `SecureServer` for secure TLS connections + (#55 by @clue) + +* Add functional integration tests + (#54 by @clue) + +## 0.4.4 (2016-12-19) + +* Feature / Fix: `ConnectionInterface` should extend `DuplexStreamInterface` + documentation + (#50 by @clue) + +* Feature / Fix: Improve test suite and switch to normal stream handler + (#51 by @clue) + +* Feature: Add examples + (#49 by @clue) + +## 0.4.3 (2016-03-01) + +* Bug fix: Suppress errors on stream_socket_accept to prevent PHP from crashing +* Support for PHP7 and HHVM +* Support PHP 5.3 again + +## 0.4.2 (2014-05-25) + +* Verify stream is a valid resource in Connection + +## 0.4.1 (2014-04-13) + +* Bug fix: Check read buffer for data before shutdown signal and end emit (@ArtyDev) +* Bug fix: v0.3.4 changes merged for v0.4.1 + +## 0.3.4 (2014-03-30) + +* Bug fix: Reset socket to non-blocking after shutting down (PHP bug) + +## 0.4.0 (2014-02-02) + +* BC break: Bump minimum PHP version to PHP 5.4, remove 5.3 specific hacks +* BC break: Update to React/Promise 2.0 +* BC break: Update to Evenement 2.0 +* Dependency: Autoloading and filesystem structure now PSR-4 instead of PSR-0 +* Bump React dependencies to v0.4 + +## 0.3.3 (2013-07-08) + +* Version bump + +## 0.3.2 (2013-05-10) + +* Version bump + +## 0.3.1 (2013-04-21) + +* Feature: Support binding to IPv6 addresses (@clue) + +## 0.3.0 (2013-04-14) + +* Bump React dependencies to v0.3 + +## 0.2.6 (2012-12-26) + +* Version bump + +## 0.2.3 (2012-11-14) + +* Version bump + +## 0.2.0 (2012-09-10) + +* Bump React dependencies to v0.2 + +## 0.1.1 (2012-07-12) + +* Version bump + +## 0.1.0 (2012-07-11) + +* First tagged release diff --git a/assets/php/vendor/react/socket/LICENSE b/assets/php/vendor/react/socket/LICENSE new file mode 100644 index 0000000..a808108 --- /dev/null +++ b/assets/php/vendor/react/socket/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2012 Igor Wiedler, Chris Boden + +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/assets/php/vendor/react/socket/README.md b/assets/php/vendor/react/socket/README.md new file mode 100644 index 0000000..e8b53a0 --- /dev/null +++ b/assets/php/vendor/react/socket/README.md @@ -0,0 +1,1419 @@ +# Socket + +[](https://travis-ci.org/reactphp/socket) + +Async, streaming plaintext TCP/IP and secure TLS socket server and client +connections for [ReactPHP](https://reactphp.org/). + +The socket library provides re-usable interfaces for a socket-layer +server and client based on the [`EventLoop`](https://github.com/reactphp/event-loop) +and [`Stream`](https://github.com/reactphp/stream) components. +Its server component allows you to build networking servers that accept incoming +connections from networking clients (such as an HTTP server). +Its client component allows you to build networking clients that establish +outgoing connections to networking servers (such as an HTTP or database client). +This library provides async, streaming means for all of this, so you can +handle multiple concurrent connections without blocking. + +**Table of Contents** + +* [Quickstart example](#quickstart-example) +* [Connection usage](#connection-usage) + * [ConnectionInterface](#connectioninterface) + * [getRemoteAddress()](#getremoteaddress) + * [getLocalAddress()](#getlocaladdress) +* [Server usage](#server-usage) + * [ServerInterface](#serverinterface) + * [connection event](#connection-event) + * [error event](#error-event) + * [getAddress()](#getaddress) + * [pause()](#pause) + * [resume()](#resume) + * [close()](#close) + * [Server](#server) + * [Advanced server usage](#advanced-server-usage) + * [TcpServer](#tcpserver) + * [SecureServer](#secureserver) + * [UnixServer](#unixserver) + * [LimitingServer](#limitingserver) + * [getConnections()](#getconnections) +* [Client usage](#client-usage) + * [ConnectorInterface](#connectorinterface) + * [connect()](#connect) + * [Connector](#connector) + * [Advanced client usage](#advanced-client-usage) + * [TcpConnector](#tcpconnector) + * [DnsConnector](#dnsconnector) + * [SecureConnector](#secureconnector) + * [TimeoutConnector](#timeoutconnector) + * [UnixConnector](#unixconnector) + * [FixUriConnector](#fixeduriconnector) +* [Install](#install) +* [Tests](#tests) +* [License](#license) + +## Quickstart example + +Here is a server that closes the connection if you send it anything: + +```php +$loop = React\EventLoop\Factory::create(); +$socket = new React\Socket\Server('127.0.0.1:8080', $loop); + +$socket->on('connection', function (ConnectionInterface $conn) { + $conn->write("Hello " . $conn->getRemoteAddress() . "!\n"); + $conn->write("Welcome to this amazing server!\n"); + $conn->write("Here's a tip: don't say anything.\n"); + + $conn->on('data', function ($data) use ($conn) { + $conn->close(); + }); +}); + +$loop->run(); +``` + +See also the [examples](examples). + +Here's a client that outputs the output of said server and then attempts to +send it a string: + +```php +$loop = React\EventLoop\Factory::create(); +$connector = new React\Socket\Connector($loop); + +$connector->connect('127.0.0.1:8080')->then(function (ConnectionInterface $conn) use ($loop) { + $conn->pipe(new React\Stream\WritableResourceStream(STDOUT, $loop)); + $conn->write("Hello World!\n"); +}); + +$loop->run(); +``` + +## Connection usage + +### ConnectionInterface + +The `ConnectionInterface` is used to represent any incoming and outgoing +connection, such as a normal TCP/IP connection. + +An incoming or outgoing connection is a duplex stream (both readable and +writable) that implements React's +[`DuplexStreamInterface`](https://github.com/reactphp/stream#duplexstreaminterface). +It contains additional properties for the local and remote address (client IP) +where this connection has been established to/from. + +Most commonly, instances implementing this `ConnectionInterface` are emitted +by all classes implementing the [`ServerInterface`](#serverinterface) and +used by all classes implementing the [`ConnectorInterface`](#connectorinterface). + +Because the `ConnectionInterface` implements the underlying +[`DuplexStreamInterface`](https://github.com/reactphp/stream#duplexstreaminterface) +you can use any of its events and methods as usual: + +```php +$connection->on('data', function ($chunk) { + echo $chunk; +}); + +$connection->on('end', function () { + echo 'ended'; +}); + +$connection->on('error', function (Exception $e) { + echo 'error: ' . $e->getMessage(); +}); + +$connection->on('close', function () { + echo 'closed'; +}); + +$connection->write($data); +$connection->end($data = null); +$connection->close(); +// … +``` + +For more details, see the +[`DuplexStreamInterface`](https://github.com/reactphp/stream#duplexstreaminterface). + +#### getRemoteAddress() + +The `getRemoteAddress(): ?string` method returns the full remote address +(URI) where this connection has been established with. + +```php +$address = $connection->getRemoteAddress(); +echo 'Connection with ' . $address . PHP_EOL; +``` + +If the remote address can not be determined or is unknown at this time (such as +after the connection has been closed), it MAY return a `NULL` value instead. + +Otherwise, it will return the full address (URI) as a string value, such +as `tcp://127.0.0.1:8080`, `tcp://[::1]:80`, `tls://127.0.0.1:443`, +`unix://example.sock` or `unix:///path/to/example.sock`. +Note that individual URI components are application specific and depend +on the underlying transport protocol. + +If this is a TCP/IP based connection and you only want the remote IP, you may +use something like this: + +```php +$address = $connection->getRemoteAddress(); +$ip = trim(parse_url($address, PHP_URL_HOST), '[]'); +echo 'Connection with ' . $ip . PHP_EOL; +``` + +#### getLocalAddress() + +The `getLocalAddress(): ?string` method returns the full local address +(URI) where this connection has been established with. + +```php +$address = $connection->getLocalAddress(); +echo 'Connection with ' . $address . PHP_EOL; +``` + +If the local address can not be determined or is unknown at this time (such as +after the connection has been closed), it MAY return a `NULL` value instead. + +Otherwise, it will return the full address (URI) as a string value, such +as `tcp://127.0.0.1:8080`, `tcp://[::1]:80`, `tls://127.0.0.1:443`, +`unix://example.sock` or `unix:///path/to/example.sock`. +Note that individual URI components are application specific and depend +on the underlying transport protocol. + +This method complements the [`getRemoteAddress()`](#getremoteaddress) method, +so they should not be confused. + +If your `TcpServer` instance is listening on multiple interfaces (e.g. using +the address `0.0.0.0`), you can use this method to find out which interface +actually accepted this connection (such as a public or local interface). + +If your system has multiple interfaces (e.g. a WAN and a LAN interface), +you can use this method to find out which interface was actually +used for this connection. + +## Server usage + +### ServerInterface + +The `ServerInterface` is responsible for providing an interface for accepting +incoming streaming connections, such as a normal TCP/IP connection. + +Most higher-level components (such as a HTTP server) accept an instance +implementing this interface to accept incoming streaming connections. +This is usually done via dependency injection, so it's fairly simple to actually +swap this implementation against any other implementation of this interface. +This means that you SHOULD typehint against this interface instead of a concrete +implementation of this interface. + +Besides defining a few methods, this interface also implements the +[`EventEmitterInterface`](https://github.com/igorw/evenement) +which allows you to react to certain events. + +#### connection event + +The `connection` event will be emitted whenever a new connection has been +established, i.e. a new client connects to this server socket: + +```php +$server->on('connection', function (ConnectionInterface $connection) { + echo 'new connection' . PHP_EOL; +}); +``` + +See also the [`ConnectionInterface`](#connectioninterface) for more details +about handling the incoming connection. + +#### error event + +The `error` event will be emitted whenever there's an error accepting a new +connection from a client. + +```php +$server->on('error', function (Exception $e) { + echo 'error: ' . $e->getMessage() . PHP_EOL; +}); +``` + +Note that this is not a fatal error event, i.e. the server keeps listening for +new connections even after this event. + + +#### getAddress() + +The `getAddress(): ?string` method can be used to +return the full address (URI) this server is currently listening on. + +```php +$address = $server->getAddress(); +echo 'Server listening on ' . $address . PHP_EOL; +``` + +If the address can not be determined or is unknown at this time (such as +after the socket has been closed), it MAY return a `NULL` value instead. + +Otherwise, it will return the full address (URI) as a string value, such +as `tcp://127.0.0.1:8080`, `tcp://[::1]:80`, `tls://127.0.0.1:443` +`unix://example.sock` or `unix:///path/to/example.sock`. +Note that individual URI components are application specific and depend +on the underlying transport protocol. + +If this is a TCP/IP based server and you only want the local port, you may +use something like this: + +```php +$address = $server->getAddress(); +$port = parse_url($address, PHP_URL_PORT); +echo 'Server listening on port ' . $port . PHP_EOL; +``` + +#### pause() + +The `pause(): void` method can be used to +pause accepting new incoming connections. + +Removes the socket resource from the EventLoop and thus stop accepting +new connections. Note that the listening socket stays active and is not +closed. + +This means that new incoming connections will stay pending in the +operating system backlog until its configurable backlog is filled. +Once the backlog is filled, the operating system may reject further +incoming connections until the backlog is drained again by resuming +to accept new connections. + +Once the server is paused, no futher `connection` events SHOULD +be emitted. + +```php +$server->pause(); + +$server->on('connection', assertShouldNeverCalled()); +``` + +This method is advisory-only, though generally not recommended, the +server MAY continue emitting `connection` events. + +Unless otherwise noted, a successfully opened server SHOULD NOT start +in paused state. + +You can continue processing events by calling `resume()` again. + +Note that both methods can be called any number of times, in particular +calling `pause()` more than once SHOULD NOT have any effect. +Similarly, calling this after `close()` is a NO-OP. + +#### resume() + +The `resume(): void` method can be used to +resume accepting new incoming connections. + +Re-attach the socket resource to the EventLoop after a previous `pause()`. + +```php +$server->pause(); + +$loop->addTimer(1.0, function () use ($server) { + $server->resume(); +}); +``` + +Note that both methods can be called any number of times, in particular +calling `resume()` without a prior `pause()` SHOULD NOT have any effect. +Similarly, calling this after `close()` is a NO-OP. + +#### close() + +The `close(): void` method can be used to +shut down this listening socket. + +This will stop listening for new incoming connections on this socket. + +```php +echo 'Shutting down server socket' . PHP_EOL; +$server->close(); +``` + +Calling this method more than once on the same instance is a NO-OP. + +### Server + +The `Server` class is the main class in this package that implements the +[`ServerInterface`](#serverinterface) and allows you to accept incoming +streaming connections, such as plaintext TCP/IP or secure TLS connection streams. +Connections can also be accepted on Unix domain sockets. + +```php +$server = new Server(8080, $loop); +``` + +As above, the `$uri` parameter can consist of only a port, in which case the +server will default to listening on the localhost address `127.0.0.1`, +which means it will not be reachable from outside of this system. + +In order to use a random port assignment, you can use the port `0`: + +```php +$server = new Server(0, $loop); +$address = $server->getAddress(); +``` + +In order to change the host the socket is listening on, you can provide an IP +address through the first parameter provided to the constructor, optionally +preceded by the `tcp://` scheme: + +```php +$server = new Server('192.168.0.1:8080', $loop); +``` + +If you want to listen on an IPv6 address, you MUST enclose the host in square +brackets: + +```php +$server = new Server('[::1]:8080', $loop); +``` + +To listen on a Unix domain socket (UDS) path, you MUST prefix the URI with the +`unix://` scheme: + +```php +$server = new Server('unix:///tmp/server.sock', $loop); +``` + +If the given URI is invalid, does not contain a port, any other scheme or if it +contains a hostname, it will throw an `InvalidArgumentException`: + +```php +// throws InvalidArgumentException due to missing port +$server = new Server('127.0.0.1', $loop); +``` + +If the given URI appears to be valid, but listening on it fails (such as if port +is already in use or port below 1024 may require root access etc.), it will +throw a `RuntimeException`: + +```php +$first = new Server(8080, $loop); + +// throws RuntimeException because port is already in use +$second = new Server(8080, $loop); +``` + +> Note that these error conditions may vary depending on your system and/or + configuration. + See the exception message and code for more details about the actual error + condition. + +Optionally, you can specify [TCP socket context options](http://php.net/manual/en/context.socket.php) +for the underlying stream socket resource like this: + +```php +$server = new Server('[::1]:8080', $loop, array( + 'tcp' => array( + 'backlog' => 200, + 'so_reuseport' => true, + 'ipv6_v6only' => true + ) +)); +``` + +> Note that available [socket context options](http://php.net/manual/en/context.socket.php), + their defaults and effects of changing these may vary depending on your system + and/or PHP version. + Passing unknown context options has no effect. + For BC reasons, you can also pass the TCP socket context options as a simple + array without wrapping this in another array under the `tcp` key. + +You can start a secure TLS (formerly known as SSL) server by simply prepending +the `tls://` URI scheme. +Internally, it will wait for plaintext TCP/IP connections and then performs a +TLS handshake for each connection. +It thus requires valid [TLS context options](http://php.net/manual/en/context.ssl.php), +which in its most basic form may look something like this if you're using a +PEM encoded certificate file: + +```php +$server = new Server('tls://127.0.0.1:8080', $loop, array( + 'tls' => array( + 'local_cert' => 'server.pem' + ) +)); +``` + +> Note that the certificate file will not be loaded on instantiation but when an + incoming connection initializes its TLS context. + This implies that any invalid certificate file paths or contents will only cause + an `error` event at a later time. + +If your private key is encrypted with a passphrase, you have to specify it +like this: + +```php +$server = new Server('tls://127.0.0.1:8000', $loop, array( + 'tls' => array( + 'local_cert' => 'server.pem', + 'passphrase' => 'secret' + ) +)); +``` + +By default, this server supports TLSv1.0+ and excludes support for legacy +SSLv2/SSLv3. As of PHP 5.6+ you can also explicitly choose the TLS version you +want to negotiate with the remote side: + +```php +$server = new Server('tls://127.0.0.1:8000', $loop, array( + 'tls' => array( + 'local_cert' => 'server.pem', + 'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_SERVER + ) +)); +``` + +> Note that available [TLS context options](http://php.net/manual/en/context.ssl.php), + their defaults and effects of changing these may vary depending on your system + and/or PHP version. + The outer context array allows you to also use `tcp` (and possibly more) + context options at the same time. + Passing unknown context options has no effect. + If you do not use the `tls://` scheme, then passing `tls` context options + has no effect. + +Whenever a client connects, it will emit a `connection` event with a connection +instance implementing [`ConnectionInterface`](#connectioninterface): + +```php +$server->on('connection', function (ConnectionInterface $connection) { + echo 'Plaintext connection from ' . $connection->getRemoteAddress() . PHP_EOL; + + $connection->write('hello there!' . PHP_EOL); + … +}); +``` + +See also the [`ServerInterface`](#serverinterface) for more details. + +> Note that the `Server` class is a concrete implementation for TCP/IP sockets. + If you want to typehint in your higher-level protocol implementation, you SHOULD + use the generic [`ServerInterface`](#serverinterface) instead. + +### Advanced server usage + +#### TcpServer + +The `TcpServer` class implements the [`ServerInterface`](#serverinterface) and +is responsible for accepting plaintext TCP/IP connections. + +```php +$server = new TcpServer(8080, $loop); +``` + +As above, the `$uri` parameter can consist of only a port, in which case the +server will default to listening on the localhost address `127.0.0.1`, +which means it will not be reachable from outside of this system. + +In order to use a random port assignment, you can use the port `0`: + +```php +$server = new TcpServer(0, $loop); +$address = $server->getAddress(); +``` + +In order to change the host the socket is listening on, you can provide an IP +address through the first parameter provided to the constructor, optionally +preceded by the `tcp://` scheme: + +```php +$server = new TcpServer('192.168.0.1:8080', $loop); +``` + +If you want to listen on an IPv6 address, you MUST enclose the host in square +brackets: + +```php +$server = new TcpServer('[::1]:8080', $loop); +``` + +If the given URI is invalid, does not contain a port, any other scheme or if it +contains a hostname, it will throw an `InvalidArgumentException`: + +```php +// throws InvalidArgumentException due to missing port +$server = new TcpServer('127.0.0.1', $loop); +``` + +If the given URI appears to be valid, but listening on it fails (such as if port +is already in use or port below 1024 may require root access etc.), it will +throw a `RuntimeException`: + +```php +$first = new TcpServer(8080, $loop); + +// throws RuntimeException because port is already in use +$second = new TcpServer(8080, $loop); +``` + +> Note that these error conditions may vary depending on your system and/or +configuration. +See the exception message and code for more details about the actual error +condition. + +Optionally, you can specify [socket context options](http://php.net/manual/en/context.socket.php) +for the underlying stream socket resource like this: + +```php +$server = new TcpServer('[::1]:8080', $loop, array( + 'backlog' => 200, + 'so_reuseport' => true, + 'ipv6_v6only' => true +)); +``` + +> Note that available [socket context options](http://php.net/manual/en/context.socket.php), +their defaults and effects of changing these may vary depending on your system +and/or PHP version. +Passing unknown context options has no effect. + +Whenever a client connects, it will emit a `connection` event with a connection +instance implementing [`ConnectionInterface`](#connectioninterface): + +```php +$server->on('connection', function (ConnectionInterface $connection) { + echo 'Plaintext connection from ' . $connection->getRemoteAddress() . PHP_EOL; + + $connection->write('hello there!' . PHP_EOL); + … +}); +``` + +See also the [`ServerInterface`](#serverinterface) for more details. + +#### SecureServer + +The `SecureServer` class implements the [`ServerInterface`](#serverinterface) +and is responsible for providing a secure TLS (formerly known as SSL) server. + +It does so by wrapping a [`TcpServer`](#tcpserver) instance which waits for plaintext +TCP/IP connections and then performs a TLS handshake for each connection. +It thus requires valid [TLS context options](http://php.net/manual/en/context.ssl.php), +which in its most basic form may look something like this if you're using a +PEM encoded certificate file: + +```php +$server = new TcpServer(8000, $loop); +$server = new SecureServer($server, $loop, array( + 'local_cert' => 'server.pem' +)); +``` + +> Note that the certificate file will not be loaded on instantiation but when an +incoming connection initializes its TLS context. +This implies that any invalid certificate file paths or contents will only cause +an `error` event at a later time. + +If your private key is encrypted with a passphrase, you have to specify it +like this: + +```php +$server = new TcpServer(8000, $loop); +$server = new SecureServer($server, $loop, array( + 'local_cert' => 'server.pem', + 'passphrase' => 'secret' +)); +``` + +By default, this server supports TLSv1.0+ and excludes support for legacy +SSLv2/SSLv3. As of PHP 5.6+ you can also explicitly choose the TLS version you +want to negotiate with the remote side: + +```php +$server = new TcpServer(8000, $loop); +$server = new SecureServer($server, $loop, array( + 'local_cert' => 'server.pem', + 'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_SERVER +)); +``` + +> Note that available [TLS context options](http://php.net/manual/en/context.ssl.php), +their defaults and effects of changing these may vary depending on your system +and/or PHP version. +Passing unknown context options has no effect. + +Whenever a client completes the TLS handshake, it will emit a `connection` event +with a connection instance implementing [`ConnectionInterface`](#connectioninterface): + +```php +$server->on('connection', function (ConnectionInterface $connection) { + echo 'Secure connection from' . $connection->getRemoteAddress() . PHP_EOL; + + $connection->write('hello there!' . PHP_EOL); + … +}); +``` + +Whenever a client fails to perform a successful TLS handshake, it will emit an +`error` event and then close the underlying TCP/IP connection: + +```php +$server->on('error', function (Exception $e) { + echo 'Error' . $e->getMessage() . PHP_EOL; +}); +``` + +See also the [`ServerInterface`](#serverinterface) for more details. + +Note that the `SecureServer` class is a concrete implementation for TLS sockets. +If you want to typehint in your higher-level protocol implementation, you SHOULD +use the generic [`ServerInterface`](#serverinterface) instead. + +> Advanced usage: Despite allowing any `ServerInterface` as first parameter, +you SHOULD pass a `TcpServer` instance as first parameter, unless you +know what you're doing. +Internally, the `SecureServer` has to set the required TLS context options on +the underlying stream resources. +These resources are not exposed through any of the interfaces defined in this +package, but only through the internal `Connection` class. +The `TcpServer` class is guaranteed to emit connections that implement +the `ConnectionInterface` and uses the internal `Connection` class in order to +expose these underlying resources. +If you use a custom `ServerInterface` and its `connection` event does not +meet this requirement, the `SecureServer` will emit an `error` event and +then close the underlying connection. + +#### UnixServer + +The `UnixServer` class implements the [`ServerInterface`](#serverinterface) and +is responsible for accepting connections on Unix domain sockets (UDS). + +```php +$server = new UnixServer('/tmp/server.sock', $loop); +``` + +As above, the `$uri` parameter can consist of only a socket path or socket path +prefixed by the `unix://` scheme. + +If the given URI appears to be valid, but listening on it fails (such as if the +socket is already in use or the file not accessible etc.), it will throw a +`RuntimeException`: + +```php +$first = new UnixServer('/tmp/same.sock', $loop); + +// throws RuntimeException because socket is already in use +$second = new UnixServer('/tmp/same.sock', $loop); +``` + +Whenever a client connects, it will emit a `connection` event with a connection +instance implementing [`ConnectionInterface`](#connectioninterface): + +```php +$server->on('connection', function (ConnectionInterface $connection) { + echo 'New connection' . PHP_EOL; + + $connection->write('hello there!' . PHP_EOL); + … +}); +``` + +See also the [`ServerInterface`](#serverinterface) for more details. + +#### LimitingServer + +The `LimitingServer` decorator wraps a given `ServerInterface` and is responsible +for limiting and keeping track of open connections to this server instance. + +Whenever the underlying server emits a `connection` event, it will check its +limits and then either + - keep track of this connection by adding it to the list of + open connections and then forward the `connection` event + - or reject (close) the connection when its limits are exceeded and will + forward an `error` event instead. + +Whenever a connection closes, it will remove this connection from the list of +open connections. + +```php +$server = new LimitingServer($server, 100); +$server->on('connection', function (ConnectionInterface $connection) { + $connection->write('hello there!' . PHP_EOL); + … +}); +``` + +See also the [second example](examples) for more details. + +You have to pass a maximum number of open connections to ensure +the server will automatically reject (close) connections once this limit +is exceeded. In this case, it will emit an `error` event to inform about +this and no `connection` event will be emitted. + +```php +$server = new LimitingServer($server, 100); +$server->on('connection', function (ConnectionInterface $connection) { + $connection->write('hello there!' . PHP_EOL); + … +}); +``` + +You MAY pass a `null` limit in order to put no limit on the number of +open connections and keep accepting new connection until you run out of +operating system resources (such as open file handles). This may be +useful if you do not want to take care of applying a limit but still want +to use the `getConnections()` method. + +You can optionally configure the server to pause accepting new +connections once the connection limit is reached. In this case, it will +pause the underlying server and no longer process any new connections at +all, thus also no longer closing any excessive connections. +The underlying operating system is responsible for keeping a backlog of +pending connections until its limit is reached, at which point it will +start rejecting further connections. +Once the server is below the connection limit, it will continue consuming +connections from the backlog and will process any outstanding data on +each connection. +This mode may be useful for some protocols that are designed to wait for +a response message (such as HTTP), but may be less useful for other +protocols that demand immediate responses (such as a "welcome" message in +an interactive chat). + +```php +$server = new LimitingServer($server, 100, true); +$server->on('connection', function (ConnectionInterface $connection) { + $connection->write('hello there!' . PHP_EOL); + … +}); +``` + +##### getConnections() + +The `getConnections(): ConnectionInterface[]` method can be used to +return an array with all currently active connections. + +```php +foreach ($server->getConnection() as $connection) { + $connection->write('Hi!'); +} +``` + +## Client usage + +### ConnectorInterface + +The `ConnectorInterface` is responsible for providing an interface for +establishing streaming connections, such as a normal TCP/IP connection. + +This is the main interface defined in this package and it is used throughout +React's vast ecosystem. + +Most higher-level components (such as HTTP, database or other networking +service clients) accept an instance implementing this interface to create their +TCP/IP connection to the underlying networking service. +This is usually done via dependency injection, so it's fairly simple to actually +swap this implementation against any other implementation of this interface. + +The interface only offers a single method: + +#### connect() + +The `connect(string $uri): PromiseInterface<ConnectionInterface, Exception>` method +can be used to create a streaming connection to the given remote address. + +It returns a [Promise](https://github.com/reactphp/promise) which either +fulfills with a stream implementing [`ConnectionInterface`](#connectioninterface) +on success or rejects with an `Exception` if the connection is not successful: + +```php +$connector->connect('google.com:443')->then( + function (ConnectionInterface $connection) { + // connection successfully established + }, + function (Exception $error) { + // failed to connect due to $error + } +); +``` + +See also [`ConnectionInterface`](#connectioninterface) for more details. + +The returned Promise MUST be implemented in such a way that it can be +cancelled when it is still pending. Cancelling a pending promise MUST +reject its value with an `Exception`. It SHOULD clean up any underlying +resources and references as applicable: + +```php +$promise = $connector->connect($uri); + +$promise->cancel(); +``` + +### Connector + +The `Connector` class is the main class in this package that implements the +[`ConnectorInterface`](#connectorinterface) and allows you to create streaming connections. + +You can use this connector to create any kind of streaming connections, such +as plaintext TCP/IP, secure TLS or local Unix connection streams. + +It binds to the main event loop and can be used like this: + +```php +$loop = React\EventLoop\Factory::create(); +$connector = new Connector($loop); + +$connector->connect($uri)->then(function (ConnectionInterface $connection) { + $connection->write('...'); + $connection->end(); +}); + +$loop->run(); +``` + +In order to create a plaintext TCP/IP connection, you can simply pass a host +and port combination like this: + +```php +$connector->connect('www.google.com:80')->then(function (ConnectionInterface $connection) { + $connection->write('...'); + $connection->end(); +}); +``` + +> If you do no specify a URI scheme in the destination URI, it will assume + `tcp://` as a default and establish a plaintext TCP/IP connection. + Note that TCP/IP connections require a host and port part in the destination + URI like above, all other URI components are optional. + +In order to create a secure TLS connection, you can use the `tls://` URI scheme +like this: + +```php +$connector->connect('tls://www.google.com:443')->then(function (ConnectionInterface $connection) { + $connection->write('...'); + $connection->end(); +}); +``` + +In order to create a local Unix domain socket connection, you can use the +`unix://` URI scheme like this: + +```php +$connector->connect('unix:///tmp/demo.sock')->then(function (ConnectionInterface $connection) { + $connection->write('...'); + $connection->end(); +}); +``` + +> The [`getRemoteAddress()`](#getremoteaddress) method will return the target + Unix domain socket (UDS) path as given to the `connect()` method, including + the `unix://` scheme, for example `unix:///tmp/demo.sock`. + The [`getLocalAddress()`](#getlocaladdress) method will most likely return a + `null` value as this value is not applicable to UDS connections here. + +Under the hood, the `Connector` is implemented as a *higher-level facade* +for the lower-level connectors implemented in this package. This means it +also shares all of their features and implementation details. +If you want to typehint in your higher-level protocol implementation, you SHOULD +use the generic [`ConnectorInterface`](#connectorinterface) instead. + +The `Connector` class will try to detect your system DNS settings (and uses +Google's public DNS server `8.8.8.8` as a fallback if unable to determine your +system settings) to resolve all public hostnames into underlying IP addresses by +default. +If you explicitly want to use a custom DNS server (such as a local DNS relay or +a company wide DNS server), you can set up the `Connector` like this: + +```php +$connector = new Connector($loop, array( + 'dns' => '127.0.1.1' +)); + +$connector->connect('localhost:80')->then(function (ConnectionInterface $connection) { + $connection->write('...'); + $connection->end(); +}); +``` + +If you do not want to use a DNS resolver at all and want to connect to IP +addresses only, you can also set up your `Connector` like this: + +```php +$connector = new Connector($loop, array( + 'dns' => false +)); + +$connector->connect('127.0.0.1:80')->then(function (ConnectionInterface $connection) { + $connection->write('...'); + $connection->end(); +}); +``` + +Advanced: If you need a custom DNS `Resolver` instance, you can also set up +your `Connector` like this: + +```php +$dnsResolverFactory = new React\Dns\Resolver\Factory(); +$resolver = $dnsResolverFactory->createCached('127.0.1.1', $loop); + +$connector = new Connector($loop, array( + 'dns' => $resolver +)); + +$connector->connect('localhost:80')->then(function (ConnectionInterface $connection) { + $connection->write('...'); + $connection->end(); +}); +``` + +By default, the `tcp://` and `tls://` URI schemes will use timeout value that +repects your `default_socket_timeout` ini setting (which defaults to 60s). +If you want a custom timeout value, you can simply pass this like this: + +```php +$connector = new Connector($loop, array( + 'timeout' => 10.0 +)); +``` + +Similarly, if you do not want to apply a timeout at all and let the operating +system handle this, you can pass a boolean flag like this: + +```php +$connector = new Connector($loop, array( + 'timeout' => false +)); +``` + +By default, the `Connector` supports the `tcp://`, `tls://` and `unix://` +URI schemes. If you want to explicitly prohibit any of these, you can simply +pass boolean flags like this: + +```php +// only allow secure TLS connections +$connector = new Connector($loop, array( + 'tcp' => false, + 'tls' => true, + 'unix' => false, +)); + +$connector->connect('tls://google.com:443')->then(function (ConnectionInterface $connection) { + $connection->write('...'); + $connection->end(); +}); +``` + +The `tcp://` and `tls://` also accept additional context options passed to +the underlying connectors. +If you want to explicitly pass additional context options, you can simply +pass arrays of context options like this: + +```php +// allow insecure TLS connections +$connector = new Connector($loop, array( + 'tcp' => array( + 'bindto' => '192.168.0.1:0' + ), + 'tls' => array( + 'verify_peer' => false, + 'verify_peer_name' => false + ), +)); + +$connector->connect('tls://localhost:443')->then(function (ConnectionInterface $connection) { + $connection->write('...'); + $connection->end(); +}); +``` + +By default, this connector supports TLSv1.0+ and excludes support for legacy +SSLv2/SSLv3. As of PHP 5.6+ you can also explicitly choose the TLS version you +want to negotiate with the remote side: + +```php +$connector = new Connector($loop, array( + 'tls' => array( + 'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT + ) +)); +``` + +> For more details about context options, please refer to the PHP documentation + about [socket context options](http://php.net/manual/en/context.socket.php) + and [SSL context options](http://php.net/manual/en/context.ssl.php). + +Advanced: By default, the `Connector` supports the `tcp://`, `tls://` and +`unix://` URI schemes. +For this, it sets up the required connector classes automatically. +If you want to explicitly pass custom connectors for any of these, you can simply +pass an instance implementing the `ConnectorInterface` like this: + +```php +$dnsResolverFactory = new React\Dns\Resolver\Factory(); +$resolver = $dnsResolverFactory->createCached('127.0.1.1', $loop); +$tcp = new DnsConnector(new TcpConnector($loop), $resolver); + +$tls = new SecureConnector($tcp, $loop); + +$unix = new UnixConnector($loop); + +$connector = new Connector($loop, array( + 'tcp' => $tcp, + 'tls' => $tls, + 'unix' => $unix, + + 'dns' => false, + 'timeout' => false, +)); + +$connector->connect('google.com:80')->then(function (ConnectionInterface $connection) { + $connection->write('...'); + $connection->end(); +}); +``` + +> Internally, the `tcp://` connector will always be wrapped by the DNS resolver, + unless you disable DNS like in the above example. In this case, the `tcp://` + connector receives the actual hostname instead of only the resolved IP address + and is thus responsible for performing the lookup. + Internally, the automatically created `tls://` connector will always wrap the + underlying `tcp://` connector for establishing the underlying plaintext + TCP/IP connection before enabling secure TLS mode. If you want to use a custom + underlying `tcp://` connector for secure TLS connections only, you may + explicitly pass a `tls://` connector like above instead. + Internally, the `tcp://` and `tls://` connectors will always be wrapped by + `TimeoutConnector`, unless you disable timeouts like in the above example. + +### Advanced client usage + +#### TcpConnector + +The `React\Socket\TcpConnector` class implements the +[`ConnectorInterface`](#connectorinterface) and allows you to create plaintext +TCP/IP connections to any IP-port-combination: + +```php +$tcpConnector = new React\Socket\TcpConnector($loop); + +$tcpConnector->connect('127.0.0.1:80')->then(function (ConnectionInterface $connection) { + $connection->write('...'); + $connection->end(); +}); + +$loop->run(); +``` + +See also the [examples](examples). + +Pending connection attempts can be cancelled by cancelling its pending promise like so: + +```php +$promise = $tcpConnector->connect('127.0.0.1:80'); + +$promise->cancel(); +``` + +Calling `cancel()` on a pending promise will close the underlying socket +resource, thus cancelling the pending TCP/IP connection, and reject the +resulting promise. + +You can optionally pass additional +[socket context options](http://php.net/manual/en/context.socket.php) +to the constructor like this: + +```php +$tcpConnector = new React\Socket\TcpConnector($loop, array( + 'bindto' => '192.168.0.1:0' +)); +``` + +Note that this class only allows you to connect to IP-port-combinations. +If the given URI is invalid, does not contain a valid IP address and port +or contains any other scheme, it will reject with an +`InvalidArgumentException`: + +If the given URI appears to be valid, but connecting to it fails (such as if +the remote host rejects the connection etc.), it will reject with a +`RuntimeException`. + +If you want to connect to hostname-port-combinations, see also the following chapter. + +> Advanced usage: Internally, the `TcpConnector` allocates an empty *context* +resource for each stream resource. +If the destination URI contains a `hostname` query parameter, its value will +be used to set up the TLS peer name. +This is used by the `SecureConnector` and `DnsConnector` to verify the peer +name and can also be used if you want a custom TLS peer name. + +#### DnsConnector + +The `DnsConnector` class implements the +[`ConnectorInterface`](#connectorinterface) and allows you to create plaintext +TCP/IP connections to any hostname-port-combination. + +It does so by decorating a given `TcpConnector` instance so that it first +looks up the given domain name via DNS (if applicable) and then establishes the +underlying TCP/IP connection to the resolved target IP address. + +Make sure to set up your DNS resolver and underlying TCP connector like this: + +```php +$dnsResolverFactory = new React\Dns\Resolver\Factory(); +$dns = $dnsResolverFactory->createCached('8.8.8.8', $loop); + +$dnsConnector = new React\Socket\DnsConnector($tcpConnector, $dns); + +$dnsConnector->connect('www.google.com:80')->then(function (ConnectionInterface $connection) { + $connection->write('...'); + $connection->end(); +}); + +$loop->run(); +``` + +See also the [examples](examples). + +Pending connection attempts can be cancelled by cancelling its pending promise like so: + +```php +$promise = $dnsConnector->connect('www.google.com:80'); + +$promise->cancel(); +``` + +Calling `cancel()` on a pending promise will cancel the underlying DNS lookup +and/or the underlying TCP/IP connection and reject the resulting promise. + +> Advanced usage: Internally, the `DnsConnector` relies on a `Resolver` to +look up the IP address for the given hostname. +It will then replace the hostname in the destination URI with this IP and +append a `hostname` query parameter and pass this updated URI to the underlying +connector. +The underlying connector is thus responsible for creating a connection to the +target IP address, while this query parameter can be used to check the original +hostname and is used by the `TcpConnector` to set up the TLS peer name. +If a `hostname` is given explicitly, this query parameter will not be modified, +which can be useful if you want a custom TLS peer name. + +#### SecureConnector + +The `SecureConnector` class implements the +[`ConnectorInterface`](#connectorinterface) and allows you to create secure +TLS (formerly known as SSL) connections to any hostname-port-combination. + +It does so by decorating a given `DnsConnector` instance so that it first +creates a plaintext TCP/IP connection and then enables TLS encryption on this +stream. + +```php +$secureConnector = new React\Socket\SecureConnector($dnsConnector, $loop); + +$secureConnector->connect('www.google.com:443')->then(function (ConnectionInterface $connection) { + $connection->write("GET / HTTP/1.0\r\nHost: www.google.com\r\n\r\n"); + ... +}); + +$loop->run(); +``` + +See also the [examples](examples). + +Pending connection attempts can be cancelled by cancelling its pending promise like so: + +```php +$promise = $secureConnector->connect('www.google.com:443'); + +$promise->cancel(); +``` + +Calling `cancel()` on a pending promise will cancel the underlying TCP/IP +connection and/or the SSL/TLS negotiation and reject the resulting promise. + +You can optionally pass additional +[SSL context options](http://php.net/manual/en/context.ssl.php) +to the constructor like this: + +```php +$secureConnector = new React\Socket\SecureConnector($dnsConnector, $loop, array( + 'verify_peer' => false, + 'verify_peer_name' => false +)); +``` + +By default, this connector supports TLSv1.0+ and excludes support for legacy +SSLv2/SSLv3. As of PHP 5.6+ you can also explicitly choose the TLS version you +want to negotiate with the remote side: + +```php +$secureConnector = new React\Socket\SecureConnector($dnsConnector, $loop, array( + 'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT +)); +``` + +> Advanced usage: Internally, the `SecureConnector` relies on setting up the +required *context options* on the underlying stream resource. +It should therefor be used with a `TcpConnector` somewhere in the connector +stack so that it can allocate an empty *context* resource for each stream +resource and verify the peer name. +Failing to do so may result in a TLS peer name mismatch error or some hard to +trace race conditions, because all stream resources will use a single, shared +*default context* resource otherwise. + +#### TimeoutConnector + +The `TimeoutConnector` class implements the +[`ConnectorInterface`](#connectorinterface) and allows you to add timeout +handling to any existing connector instance. + +It does so by decorating any given [`ConnectorInterface`](#connectorinterface) +instance and starting a timer that will automatically reject and abort any +underlying connection attempt if it takes too long. + +```php +$timeoutConnector = new React\Socket\TimeoutConnector($connector, 3.0, $loop); + +$timeoutConnector->connect('google.com:80')->then(function (ConnectionInterface $connection) { + // connection succeeded within 3.0 seconds +}); +``` + +See also any of the [examples](examples). + +Pending connection attempts can be cancelled by cancelling its pending promise like so: + +```php +$promise = $timeoutConnector->connect('google.com:80'); + +$promise->cancel(); +``` + +Calling `cancel()` on a pending promise will cancel the underlying connection +attempt, abort the timer and reject the resulting promise. + +#### UnixConnector + +The `UnixConnector` class implements the +[`ConnectorInterface`](#connectorinterface) and allows you to connect to +Unix domain socket (UDS) paths like this: + +```php +$connector = new React\Socket\UnixConnector($loop); + +$connector->connect('/tmp/demo.sock')->then(function (ConnectionInterface $connection) { + $connection->write("HELLO\n"); +}); + +$loop->run(); +``` + +Connecting to Unix domain sockets is an atomic operation, i.e. its promise will +settle (either resolve or reject) immediately. +As such, calling `cancel()` on the resulting promise has no effect. + +> The [`getRemoteAddress()`](#getremoteaddress) method will return the target + Unix domain socket (UDS) path as given to the `connect()` method, prepended + with the `unix://` scheme, for example `unix:///tmp/demo.sock`. + The [`getLocalAddress()`](#getlocaladdress) method will most likely return a + `null` value as this value is not applicable to UDS connections here. + +#### FixedUriConnector + +The `FixedUriConnector` class implements the +[`ConnectorInterface`](#connectorinterface) and decorates an existing Connector +to always use a fixed, preconfigured URI. + +This can be useful for consumers that do not support certain URIs, such as +when you want to explicitly connect to a Unix domain socket (UDS) path +instead of connecting to a default address assumed by an higher-level API: + +```php +$connector = new FixedUriConnector( + 'unix:///var/run/docker.sock', + new UnixConnector($loop) +); + +// destination will be ignored, actually connects to Unix domain socket +$promise = $connector->connect('localhost:80'); +``` + +## Install + +The recommended way to install this library is [through Composer](https://getcomposer.org). +[New to Composer?](https://getcomposer.org/doc/00-intro.md) + +This will install the latest supported version: + +```bash +$ composer require react/socket:^0.8.10 +``` + +See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. + +This project aims to run on any platform and thus does not require any PHP +extensions and supports running on legacy PHP 5.3 through current PHP 7+ and HHVM. +It's *highly recommended to use PHP 7+* for this project, partly due to its vast +performance improvements and partly because legacy PHP versions require several +workarounds as described below. + +Secure TLS connections received some major upgrades starting with PHP 5.6, with +the defaults now being more secure, while older versions required explicit +context options. +This library does not take responsibility over these context options, so it's +up to consumers of this library to take care of setting appropriate context +options as described above. + +All versions of PHP prior to 5.6.8 suffered from a buffering issue where reading +from a streaming TLS connection could be one `data` event behind. +This library implements a work-around to try to flush the complete incoming +data buffers on these legacy PHP versions, which has a penalty of around 10% of +throughput on all connections. +With this work-around, we have not been able to reproduce this issue anymore, +but we have seen reports of people saying this could still affect some of the +older PHP versions (`5.5.23`, `5.6.7`, and `5.6.8`). +Note that this only affects *some* higher-level streaming protocols, such as +IRC over TLS, but should not affect HTTP over TLS (HTTPS). +Further investigation of this issue is needed. +For more insights, this issue is also covered by our test suite. + +PHP < 7.1.4 (and PHP < 7.0.18) suffers from a bug when writing big +chunks of data over TLS streams at once. +We try to work around this by limiting the write chunk size to 8192 +bytes for older PHP versions only. +This is only a work-around and has a noticable performance penalty on +affected versions. + +This project also supports running on HHVM. +Note that really old HHVM < 3.8 does not support secure TLS connections, as it +lacks the required `stream_socket_enable_crypto()` function. +As such, trying to create a secure TLS connections on affected versions will +return a rejected promise instead. +This issue is also covered by our test suite, which will skip related tests +on affected versions. + +## Tests + +To run the test suite, you first need to clone this repo and then install all +dependencies [through Composer](https://getcomposer.org): + +```bash +$ composer install +``` + +To run the test suite, go to the project root and run: + +```bash +$ php vendor/bin/phpunit +``` + +The test suite also contains a number of functional integration tests that rely +on a stable internet connection. +If you do not want to run these, they can simply be skipped like this: + +```bash +$ php vendor/bin/phpunit --exclude-group internet +``` + +## License + +MIT, see [LICENSE file](LICENSE). diff --git a/assets/php/vendor/react/socket/composer.json b/assets/php/vendor/react/socket/composer.json new file mode 100644 index 0000000..bc85aab --- /dev/null +++ b/assets/php/vendor/react/socket/composer.json @@ -0,0 +1,29 @@ +{ + "name": "react/socket", + "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", + "keywords": ["async", "socket", "stream", "connection", "ReactPHP"], + "license": "MIT", + "require": { + "php": ">=5.3.0", + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "react/dns": "^0.4.13", + "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3.5", + "react/stream": "^1.0 || ^0.7.1", + "react/promise": "^2.1 || ^1.2", + "react/promise-timer": "~1.0" + }, + "require-dev": { + "clue/block-react": "^1.2", + "phpunit/phpunit": "^6.4 || ^5.7 || ^4.8.35" + }, + "autoload": { + "psr-4": { + "React\\Socket\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "React\\Tests\\Socket\\": "tests" + } + } +} diff --git a/assets/php/vendor/react/socket/examples/01-echo-server.php b/assets/php/vendor/react/socket/examples/01-echo-server.php new file mode 100644 index 0000000..2c0be57 --- /dev/null +++ b/assets/php/vendor/react/socket/examples/01-echo-server.php @@ -0,0 +1,42 @@ +<?php + +// Just start this server and connect to it. Everything you send to it will be +// sent back to you. +// +// $ php examples/01-echo-server.php 8000 +// $ telnet localhost 8000 +// +// You can also run a secure TLS echo server like this: +// +// $ php examples/01-echo-server.php tls://127.0.0.1:8000 examples/localhost.pem +// $ openssl s_client -connect localhost:8000 +// +// You can also run a Unix domain socket (UDS) server like this: +// +// $ php examples/01-echo-server.php unix:///tmp/server.sock +// $ nc -U /tmp/server.sock + +use React\EventLoop\Factory; +use React\Socket\Server; +use React\Socket\ConnectionInterface; + +require __DIR__ . '/../vendor/autoload.php'; + +$loop = Factory::create(); + +$server = new Server(isset($argv[1]) ? $argv[1] : 0, $loop, array( + 'tls' => array( + 'local_cert' => isset($argv[2]) ? $argv[2] : (__DIR__ . '/localhost.pem') + ) +)); + +$server->on('connection', function (ConnectionInterface $conn) { + echo '[' . $conn->getRemoteAddress() . ' connected]' . PHP_EOL; + $conn->pipe($conn); +}); + +$server->on('error', 'printf'); + +echo 'Listening on ' . $server->getAddress() . PHP_EOL; + +$loop->run(); diff --git a/assets/php/vendor/react/socket/examples/02-chat-server.php b/assets/php/vendor/react/socket/examples/02-chat-server.php new file mode 100644 index 0000000..46439e0 --- /dev/null +++ b/assets/php/vendor/react/socket/examples/02-chat-server.php @@ -0,0 +1,59 @@ +<?php + +// Just start this server and connect with any number of clients to it. +// Everything a client sends will be broadcasted to all connected clients. +// +// $ php examples/02-chat-server.php 8000 +// $ telnet localhost 8000 +// +// You can also run a secure TLS chat server like this: +// +// $ php examples/02-chat-server.php tls://127.0.0.1:8000 examples/localhost.pem +// $ openssl s_client -connect localhost:8000 +// +// You can also run a Unix domain socket (UDS) server like this: +// +// $ php examples/02-chat-server.php unix:///tmp/server.sock +// $ nc -U /tmp/server.sock + +use React\EventLoop\Factory; +use React\Socket\Server; +use React\Socket\ConnectionInterface; +use React\Socket\LimitingServer; + +require __DIR__ . '/../vendor/autoload.php'; + +$loop = Factory::create(); + +$server = new Server(isset($argv[1]) ? $argv[1] : 0, $loop, array( + 'tls' => array( + 'local_cert' => isset($argv[2]) ? $argv[2] : (__DIR__ . '/localhost.pem') + ) +)); + +$server = new LimitingServer($server, null); + +$server->on('connection', function (ConnectionInterface $client) use ($server) { + // whenever a new message comes in + $client->on('data', function ($data) use ($client, $server) { + // remove any non-word characters (just for the demo) + $data = trim(preg_replace('/[^\w\d \.\,\-\!\?]/u', '', $data)); + + // ignore empty messages + if ($data === '') { + return; + } + + // prefix with client IP and broadcast to all connected clients + $data = trim(parse_url($client->getRemoteAddress(), PHP_URL_HOST), '[]') . ': ' . $data . PHP_EOL; + foreach ($server->getConnections() as $connection) { + $connection->write($data); + } + }); +}); + +$server->on('error', 'printf'); + +echo 'Listening on ' . $server->getAddress() . PHP_EOL; + +$loop->run(); diff --git a/assets/php/vendor/react/socket/examples/03-http-server.php b/assets/php/vendor/react/socket/examples/03-http-server.php new file mode 100644 index 0000000..eb6d454 --- /dev/null +++ b/assets/php/vendor/react/socket/examples/03-http-server.php @@ -0,0 +1,57 @@ +<?php + +// Simple HTTP server example (for illustration purposes only). +// This shows how a plaintext TCP/IP connection is accepted to then receive an +// application level protocol message (HTTP request) and reply with an +// application level protocol message (HTTP response) in return. +// +// This example exists for illustraion purposes only. It does not actually +// parse incoming HTTP requests, so you can actually send *anything* and will +// still respond with a valid HTTP response. +// Real applications should use react/http instead! +// +// Just start this server and send a request to it: +// +// $ php examples/03-http-server.php 8000 +// $ curl -v http://localhost:8000/ +// $ ab -n1000 -c10 http://localhost:8000/ +// $ docker run -it --rm --net=host jordi/ab ab -n1000 -c10 http://localhost:8000/ +// +// You can also run a secure HTTPS echo server like this: +// +// $ php examples/03-http-server.php tls://127.0.0.1:8000 examples/localhost.pem +// $ curl -v --insecure https://localhost:8000/ +// $ ab -n1000 -c10 https://localhost:8000/ +// $ docker run -it --rm --net=host jordi/ab ab -n1000 -c10 https://localhost:8000/ +// +// You can also run a Unix domain socket (UDS) server like this: +// +// $ php examples/03-http-server.php unix:///tmp/server.sock +// $ nc -U /tmp/server.sock + +use React\EventLoop\Factory; +use React\Socket\Server; +use React\Socket\ConnectionInterface; + +require __DIR__ . '/../vendor/autoload.php'; + +$loop = Factory::create(); + +$server = new Server(isset($argv[1]) ? $argv[1] : 0, $loop, array( + 'tls' => array( + 'local_cert' => isset($argv[2]) ? $argv[2] : (__DIR__ . '/localhost.pem') + ) +)); + +$server->on('connection', function (ConnectionInterface $conn) { + $conn->once('data', function () use ($conn) { + $body = "<html><h1>Hello world!</h1></html>\r\n"; + $conn->end("HTTP/1.1 200 OK\r\nContent-Length: " . strlen($body) . "\r\nConnection: close\r\n\r\n" . $body); + }); +}); + +$server->on('error', 'printf'); + +echo 'Listening on ' . strtr($server->getAddress(), array('tcp:' => 'http:', 'tls:' => 'https:')) . PHP_EOL; + +$loop->run(); diff --git a/assets/php/vendor/react/socket/examples/11-http-client.php b/assets/php/vendor/react/socket/examples/11-http-client.php new file mode 100644 index 0000000..2b64a43 --- /dev/null +++ b/assets/php/vendor/react/socket/examples/11-http-client.php @@ -0,0 +1,36 @@ +<?php + +// Simple plaintext HTTP client example (for illustration purposes only). +// This shows how a plaintext TCP/IP connection is established to then send an +// application level protocol message (HTTP). +// Real applications should use react/http-client instead! +// +// This simple example only accepts an optional host parameter to send the +// request to. See also example #22 for proper URI parsing. +// +// $ php examples/11-http-client.php +// $ php examples/11-http-client.php reactphp.org + +use React\EventLoop\Factory; +use React\Socket\Connector; +use React\Socket\ConnectionInterface; + +$host = isset($argv[1]) ? $argv[1] : 'www.google.com'; + +require __DIR__ . '/../vendor/autoload.php'; + +$loop = Factory::create(); +$connector = new Connector($loop); + +$connector->connect($host. ':80')->then(function (ConnectionInterface $connection) use ($host) { + $connection->on('data', function ($data) { + echo $data; + }); + $connection->on('close', function () { + echo '[CLOSED]' . PHP_EOL; + }); + + $connection->write("GET / HTTP/1.0\r\nHost: $host\r\n\r\n"); +}, 'printf'); + +$loop->run(); diff --git a/assets/php/vendor/react/socket/examples/12-https-client.php b/assets/php/vendor/react/socket/examples/12-https-client.php new file mode 100644 index 0000000..6e3f279 --- /dev/null +++ b/assets/php/vendor/react/socket/examples/12-https-client.php @@ -0,0 +1,36 @@ +<?php + +// Simple secure HTTPS client example (for illustration purposes only). +// This shows how a secure TLS connection is established to then send an +// application level protocol message (HTTP). +// Real applications should use react/http-client instead +// +// This simple example only accepts an optional host parameter to send the +// request to. See also example #22 for proper URI parsing. +// +// $ php examples/12-https-client.php +// $ php examples/12-https-client.php reactphp.org + +use React\EventLoop\Factory; +use React\Socket\Connector; +use React\Socket\ConnectionInterface; + +$host = isset($argv[1]) ? $argv[1] : 'www.google.com'; + +require __DIR__ . '/../vendor/autoload.php'; + +$loop = Factory::create(); +$connector = new Connector($loop); + +$connector->connect('tls://' . $host . ':443')->then(function (ConnectionInterface $connection) use ($host) { + $connection->on('data', function ($data) { + echo $data; + }); + $connection->on('close', function () { + echo '[CLOSED]' . PHP_EOL; + }); + + $connection->write("GET / HTTP/1.0\r\nHost: $host\r\n\r\n"); +}, 'printf'); + +$loop->run(); diff --git a/assets/php/vendor/react/socket/examples/21-netcat-client.php b/assets/php/vendor/react/socket/examples/21-netcat-client.php new file mode 100644 index 0000000..9140e2c --- /dev/null +++ b/assets/php/vendor/react/socket/examples/21-netcat-client.php @@ -0,0 +1,68 @@ +<?php + +// Simple plaintext TCP/IP and secure TLS client example that pipes console I/O. +// This shows how a plaintext TCP/IP or secure TLS connection is established and +// then everything you type on STDIN will be sent and everything the server +// sends will be piped to your STDOUT. +// +// $ php examples/21-netcat-client.php www.google.com:80 +// $ php examples/21-netcat-client.php tls://www.google.com:443 + +use React\EventLoop\Factory; +use React\Socket\Connector; +use React\Socket\ConnectionInterface; +use React\Stream\ReadableResourceStream; +use React\Stream\WritableResourceStream; + +require __DIR__ . '/../vendor/autoload.php'; + +if (!defined('STDIN')) { + echo 'STDIO streams require CLI SAPI' . PHP_EOL; + exit(1); +} + +if (DIRECTORY_SEPARATOR === '\\') { + fwrite(STDERR, 'Non-blocking console I/O not supported on Microsoft Windows' . PHP_EOL); + exit(1); +} + +if (!isset($argv[1])) { + fwrite(STDERR, 'Usage error: required argument <host:port>' . PHP_EOL); + exit(1); +} + +$loop = Factory::create(); +$connector = new Connector($loop); + +$stdin = new ReadableResourceStream(STDIN, $loop); +$stdin->pause(); +$stdout = new WritableResourceStream(STDOUT, $loop); +$stderr = new WritableResourceStream(STDERR, $loop); + +$stderr->write('Connecting' . PHP_EOL); + +$connector->connect($argv[1])->then(function (ConnectionInterface $connection) use ($stdin, $stdout, $stderr) { + // pipe everything from STDIN into connection + $stdin->resume(); + $stdin->pipe($connection); + + // pipe everything from connection to STDOUT + $connection->pipe($stdout); + + // report errors to STDERR + $connection->on('error', function ($error) use ($stderr) { + $stderr->write('Stream ERROR: ' . $error . PHP_EOL); + }); + + // report closing and stop reading from input + $connection->on('close', function () use ($stderr, $stdin) { + $stderr->write('[CLOSED]' . PHP_EOL); + $stdin->close(); + }); + + $stderr->write('Connected' . PHP_EOL); +}, function ($error) use ($stderr) { + $stderr->write('Connection ERROR: ' . $error . PHP_EOL); +}); + +$loop->run(); diff --git a/assets/php/vendor/react/socket/examples/22-http-client.php b/assets/php/vendor/react/socket/examples/22-http-client.php new file mode 100644 index 0000000..fcb8107 --- /dev/null +++ b/assets/php/vendor/react/socket/examples/22-http-client.php @@ -0,0 +1,60 @@ +<?php + +// Simple plaintext HTTP and secure HTTPS client example (for illustration purposes only). +// This shows how an URI parameter can parsed to decide whether to establish +// a plaintext TCP/IP or secure TLS connection and then send an +// application level protocol message (HTTP). +// Real applications should use react/http-client instead! +// +// Unlike examples #11 and #12, this example will actually take an optional URI +// parameter and parse it to connect to the correct default port and use the +// correct transport protocol. +// +// $ php examples/22-http-client.php +// $ php examples/22-http-client.php https://reactphp.org/ + +use React\EventLoop\Factory; +use React\Socket\ConnectionInterface; +use React\Socket\Connector; +use React\Stream\WritableResourceStream; + +require __DIR__ . '/../vendor/autoload.php'; + +$uri = isset($argv[1]) ? $argv[1] : 'www.google.com'; + +if (strpos($uri, '://') === false) { + $uri = 'http://' . $uri; +} +$parts = parse_url($uri); + +if (!$parts || !isset($parts['scheme'], $parts['host'])) { + fwrite(STDERR, 'Usage error: required argument <host:port>' . PHP_EOL); + exit(1); +} + +$loop = Factory::create(); +$connector = new Connector($loop); + +if (!isset($parts['port'])) { + $parts['port'] = $parts['scheme'] === 'https' ? 443 : 80; +} + +$host = $parts['host']; +if (($parts['scheme'] === 'http' && $parts['port'] !== 80) || ($parts['scheme'] === 'https' && $parts['port'] !== 443)) { + $host .= ':' . $parts['port']; +} +$target = ($parts['scheme'] === 'https' ? 'tls' : 'tcp') . '://' . $parts['host'] . ':' . $parts['port']; +$resource = isset($parts['path']) ? $parts['path'] : '/'; +if (isset($parts['query'])) { + $resource .= '?' . $parts['query']; +} + +$stdout = new WritableResourceStream(STDOUT, $loop); + +$connector->connect($target)->then(function (ConnectionInterface $connection) use ($resource, $host, $stdout) { + $connection->pipe($stdout); + + $connection->write("GET $resource HTTP/1.0\r\nHost: $host\r\n\r\n"); +}, 'printf'); + +$loop->run(); diff --git a/assets/php/vendor/react/socket/examples/91-benchmark-server.php b/assets/php/vendor/react/socket/examples/91-benchmark-server.php new file mode 100644 index 0000000..420d474 --- /dev/null +++ b/assets/php/vendor/react/socket/examples/91-benchmark-server.php @@ -0,0 +1,60 @@ +<?php + +// Just start the server and connect to it. It will count the number of bytes +// sent for each connection and will print the average throughput once the +// connection closes. +// +// $ php examples/91-benchmark-server.php 8000 +// $ telnet localhost 8000 +// $ echo hello world | nc -N localhost 8000 +// $ dd if=/dev/zero bs=1M count=1000 | nc -N localhost 8000 +// +// You can also run a secure TLS benchmarking server like this: +// +// $ php examples/91-benchmark-server.php tls://127.0.0.1:8000 examples/localhost.pem +// $ openssl s_client -connect localhost:8000 +// $ echo hello world | openssl s_client -connect localhost:8000 +// $ dd if=/dev/zero bs=1M count=1000 | openssl s_client -connect localhost:8000 +// +// You can also run a Unix domain socket (UDS) server benchmark like this: +// +// $ php examples/91-benchmark-server.php unix:///tmp/server.sock +// $ nc -N -U /tmp/server.sock +// $ dd if=/dev/zero bs=1M count=1000 | nc -N -U /tmp/server.sock + +use React\EventLoop\Factory; +use React\Socket\Server; +use React\Socket\ConnectionInterface; + +require __DIR__ . '/../vendor/autoload.php'; + +$loop = Factory::create(); + +$server = new Server(isset($argv[1]) ? $argv[1] : 0, $loop, array( + 'tls' => array( + 'local_cert' => isset($argv[2]) ? $argv[2] : (__DIR__ . '/localhost.pem') + ) +)); + +$server->on('connection', function (ConnectionInterface $conn) use ($loop) { + echo '[connected]' . PHP_EOL; + + // count the number of bytes received from this connection + $bytes = 0; + $conn->on('data', function ($chunk) use (&$bytes) { + $bytes += strlen($chunk); + }); + + // report average throughput once client disconnects + $t = microtime(true); + $conn->on('close', function () use ($conn, $t, &$bytes) { + $t = microtime(true) - $t; + echo '[disconnected after receiving ' . $bytes . ' bytes in ' . round($t, 3) . 's => ' . round($bytes / $t / 1024 / 1024, 1) . ' MiB/s]' . PHP_EOL; + }); +}); + +$server->on('error', 'printf'); + +echo 'Listening on ' . $server->getAddress() . PHP_EOL; + +$loop->run(); diff --git a/assets/php/vendor/react/socket/examples/99-generate-self-signed.php b/assets/php/vendor/react/socket/examples/99-generate-self-signed.php new file mode 100644 index 0000000..00f9314 --- /dev/null +++ b/assets/php/vendor/react/socket/examples/99-generate-self-signed.php @@ -0,0 +1,31 @@ +<?php + +// A very simple helper script used to generate self-signed certificates. +// Accepts the CN and an optional passphrase to encrypt the private key. +// +// $ php 10-generate-self-signed.php localhost swordfish > secret.pem + +// certificate details (Distinguished Name) +// (OpenSSL applies defaults to missing fields) +$dn = array( + "commonName" => isset($argv[1]) ? $argv[1] : "localhost", +// "countryName" => "AU", +// "stateOrProvinceName" => "Some-State", +// "localityName" => "London", +// "organizationName" => "Internet Widgits Pty Ltd", +// "organizationalUnitName" => "R&D", +// "emailAddress" => "admin@example.com" +); + +// create certificate which is valid for ~10 years +$privkey = openssl_pkey_new(); +$cert = openssl_csr_new($dn, $privkey); +$cert = openssl_csr_sign($cert, null, $privkey, 3650); + +// export public and (optionally encrypted) private key in PEM format +openssl_x509_export($cert, $out); +echo $out; + +$passphrase = isset($argv[2]) ? $argv[2] : null; +openssl_pkey_export($privkey, $out, $passphrase); +echo $out; diff --git a/assets/php/vendor/react/socket/examples/localhost.pem b/assets/php/vendor/react/socket/examples/localhost.pem new file mode 100644 index 0000000..be69279 --- /dev/null +++ b/assets/php/vendor/react/socket/examples/localhost.pem @@ -0,0 +1,49 @@ +-----BEGIN CERTIFICATE----- +MIIDfTCCAmWgAwIBAgIBADANBgkqhkiG9w0BAQUFADBZMRIwEAYDVQQDDAkxMjcu +MC4wLjExCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQK +DBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcNMTYxMjMwMTQ1OTA2WhcNMjYx +MjI4MTQ1OTA2WjBZMRIwEAYDVQQDDAkxMjcuMC4wLjExCzAJBgNVBAYTAkFVMRMw +EQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0 +eSBMdGQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC8SZWNS+Ktg0Py +W8dx5uXZ+ZUawd3wnzLMHW7EhoUpIrIdp3kDU9NezF68dOhPMJY/Kh+6btRCxWXN +2OVTqS5Xi826j3TSE07iF83JRLeveW0PcodjUBd+RzdwCWWo2pfMJz4v7x1wu1c9 +zNi6JxxpDAXTFSB4GiWsI4tFu2XmMRhfm6LRK4WPfsZIJKokdiG5fKSPDn7nrVj0 +UUXr2eBsEAzdwL14U9+mwbLdaAkz3qK3fqi8sEC09lEWm95gKMOhkQf5qvXODtT4 +wdVrrKDTyehLv0xaItnUDnXzrkMBU5QS9TQzzqSW6ZaBsSxtONEFUiXiN9dtyXsY +YCUE54G/AgMBAAGjUDBOMB0GA1UdDgQWBBQ2GRz3QsQzdXaTMnPVCKfpigA10DAf +BgNVHSMEGDAWgBQ2GRz3QsQzdXaTMnPVCKfpigA10DAMBgNVHRMEBTADAQH/MA0G +CSqGSIb3DQEBBQUAA4IBAQA77iZ4KrpPY18Ezjt0mngYAuAxunKddXYdLZ2khywN +0uI/VzYnkFVtrsC7y2jLHSxlmE2/viPPGZDUplENV2acN6JNW+tlt7/bsrQHDQw3 +7VCF27EWiDxHsaghhLkqC+kcop5YR5c0oDQTdEWEKSbow2zayUXDYbRRs76SClTe +824Yul+Ts8Mka+AX2PXDg47iZ84fJRN/nKavcJUTJ2iS1uYw0GNnFMge/uwsfMR3 +V47qN0X5emky8fcq99FlMCbcy0gHAeSWAjClgr2dd2i0LDatUbj7YmdmFcskOgII +IwGfvuWR2yPevYGAE0QgFeLHniN3RW8zmpnX/XtrJ4a7 +-----END CERTIFICATE----- +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC8SZWNS+Ktg0Py +W8dx5uXZ+ZUawd3wnzLMHW7EhoUpIrIdp3kDU9NezF68dOhPMJY/Kh+6btRCxWXN +2OVTqS5Xi826j3TSE07iF83JRLeveW0PcodjUBd+RzdwCWWo2pfMJz4v7x1wu1c9 +zNi6JxxpDAXTFSB4GiWsI4tFu2XmMRhfm6LRK4WPfsZIJKokdiG5fKSPDn7nrVj0 +UUXr2eBsEAzdwL14U9+mwbLdaAkz3qK3fqi8sEC09lEWm95gKMOhkQf5qvXODtT4 +wdVrrKDTyehLv0xaItnUDnXzrkMBU5QS9TQzzqSW6ZaBsSxtONEFUiXiN9dtyXsY +YCUE54G/AgMBAAECggEBAKiO/3FE1CMddkCLZVtUp8ShqJgRokx9WI5ecwFApAkV +ZHsjqDQQYRNmxhDUX/w0tOzLGyhde2xjJyZG29YviKsbHwu6zYwbeOzy/mkGOaK/ +g6DmmMmRs9Z6juifoQCu4GIFZ6il2adIL2vF7OeJh+eKudQj/7NFRSB7mXzNrQWK +tZY3eux5zXWmio7pgZrx1HFZQiiL9NVLwT9J7oBnaoO3fREiu5J2xBpljG9Cr0j1 +LLiVLhukWJYRlHDtGt1CzI9w8iKo44PCRzpKyxpbsOrQxeSyEWUYQRv9VHA59LC7 +tVAJTbnTX1BNHkGZkOkoOpoZLwBaM2XbbDtcOGCAZMECgYEA+mTURFQ85/pxawvk +9ndqZ+5He1u/bMLYIJDp0hdB/vgD+vw3gb2UyRwp0I6Wc6Si4FEEnbY7L0pzWsiR +43CpLs+cyLfnD9NycuIasxs5fKb/1s1nGTkRAp7x9x/ZTtEf8v4YTmmMXFHzdo7V +pv+czO89ppEDkxEtMf/b5SifhO8CgYEAwIDIUvXLduGhL+RPDwjc2SKdydXGV6om +OEdt/V8oS801Z7k8l3gHXFm7zL/MpHmh9cag+F9dHK42kw2RSjDGsBlXXiAO1Z0I +2A34OdPw/kow8fmIKWTMu3+28Kca+3RmUqeyaq0vazQ/bWMO9px+Ud3YfLo1Tn5I +li0MecAx8DECgYEAvsLceKYYtL83c09fg2oc1ctSCCgw4WJcGAtvJ9DyRZacKbXH +b/+H/+OF8879zmKqd+0hcCnqUzAMTCisBLPLIM+o6b45ufPkqKObpcJi/JWaKgLY +vf2c+Psw6o4IF6T5Cz4MNIjzF06UBknxecYZpoPJ20F1kLCwVvxPgfl99l8CgYAb +XfOcv67WTstgiJ+oroTfJamy+P5ClkDqvVTosW+EHz9ZaJ8xlXHOcj9do2LPey9I +Rp250azmF+pQS5x9JKQKgv/FtN8HBVUtigbhCb14GUoODICMCfWFLmnumoMefnTR +iV+3BLn6Dqp5vZxx+NuIffZ5/Or5JsDhALSGVomC8QKBgAi3Z/dNQrDHfkXMNn/L ++EAoLuAbFgLs76r9VGgNaRQ/q5gex2bZEGoBj4Sxvs95NUIcfD9wKT7FF8HdxARv +y3o6Bfc8Xp9So9SlFXrje+gkdEJ0rQR67d+XBuJZh86bXJHVrMwpoNL+ahLGdVSe +81oh1uCH1YPLM29hPyaohxL8 +-----END PRIVATE KEY----- diff --git a/assets/php/vendor/react/socket/examples/localhost_swordfish.pem b/assets/php/vendor/react/socket/examples/localhost_swordfish.pem new file mode 100644 index 0000000..7d1ee80 --- /dev/null +++ b/assets/php/vendor/react/socket/examples/localhost_swordfish.pem @@ -0,0 +1,51 @@ +-----BEGIN CERTIFICATE----- +MIIDfTCCAmWgAwIBAgIBADANBgkqhkiG9w0BAQUFADBZMRIwEAYDVQQDDAkxMjcu +MC4wLjExCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQK +DBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcNMTYxMjMwMTQxMDQzWhcNMjYx +MjI4MTQxMDQzWjBZMRIwEAYDVQQDDAkxMjcuMC4wLjExCzAJBgNVBAYTAkFVMRMw +EQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0 +eSBMdGQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDRXt83SrKIHr/i +3lc8O8pz6NHE1DNHJa4xg2xalXWzCEV6m1qLd9VdaLT9cJD1afNmEMBgY6RblNL/ +paJWVoR9MOUeIoYl2PrhUCxsf7h6MRtezQQe3e+n+/0XunF0JUQIZuJqbxfRk5WT +XmYnphqOZKEcistAYvFBjzl/D+Cl/nYsreADc+t9l5Vni89oTWEuqIrsM4WUZqqB +VMAakd2nZJLWIrMxq9hbW1XNukOQfcmZVFTC6CUnLq8qzGbtfZYBuMBACnL1k/E/ +yPaAgR46l14VAcndDUJBtMeL2qYuNwvXQhg3KuBmpTUpH+yzxU+4T3lmv0xXmPqu +ySH3xvW3AgMBAAGjUDBOMB0GA1UdDgQWBBRu68WTI4pVeTB7wuG9QGI3Ie441TAf +BgNVHSMEGDAWgBRu68WTI4pVeTB7wuG9QGI3Ie441TAMBgNVHRMEBTADAQH/MA0G +CSqGSIb3DQEBBQUAA4IBAQCc4pEjEHO47VRJkbHgC+c2gAVgxekkaA1czBA1uAvh +ILRda0NLlvyftbjaG0zZp2ABUCfRfksl/Pf/PzWLUMEuH/9kEW2rgP43z6YgiL6k +kBPlmAU607UjD726RPGkw8QPSXS/dWiNJ5CBpPWLpxC45pokqItYbY0ijQ5Piq09 +TchYlCX044oSRnPiP394PQ3HVdaGhJB2DnjDq3in5dVivFf8EdgzQSvp/wXy3WQs +uFSVonSnrZGY/4AgT3psGaQ6fqKb4SBoqtf5bFQvp1XNNRkuEJnS/0dygEya0c+c +aCe/1gXC2wDjx0/TekY5m1Nyw5SY6z7stOqL/ekwgejt +-----END CERTIFICATE----- +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIFDjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIG7idPRLgiHkCAggA +MBQGCCqGSIb3DQMHBAg+MLPdepHWSwSCBMgVW9LseCjfTAmF9U1qRnKsq3kIwEnW +6aERBqs/mnmEhrXgZYgcvRRK7kD12TdHt/Nz46Ymu0h+Lrvuwtl1fHQUARTk/gFh +onLhc9kjMUhLRIR007vJe3HvWOb/v+SBSDB38OpUxUwJmBVBuSaYLWVuPR6J5kUj +xOgBS049lN3E9cfrHvb3bF/epIQrU0OgfyyxEvIi5n30y+tlRn3y68PY6Qd46t4Y +UN5VZUwvJBgoRy9TGxSkiSRjhxC2PWpLYq/HMzDbcRcFF5dVAIioUd/VZ7fdgBfA +uMW4SFfpFLDUX0aaYe+ZdA5tM0Bc0cOtG8Z0sc9JYDNNcvjmSiGCi646h8F0D3O6 +JKAQMMxQGWiyQeJ979LVjtq4lJESXA8VEKz9rV03y5xunmFCLy6dGt+6GJwXgabn +OH7nvEv4GqAOqKc6E9je4JM+AF/oUazrfPse1KEEtsPKarazjCB/SKYtHyDJaavD +GGjtiU9zWwGMOgIDyNmXe3ga7/TWoGOAg5YlTr6Hbq2Y/5ycgjAgPFjuXtvnoT0+ +mF5TnNfMAqTgQsE2gjhonK1pdlOen0lN5FtoUXp3CXU0dOq0J70GiX+1YA7VDn30 +n5WNAgfOXX3l3E95jGN370pHXyli5RUNW0NZVHV+22jlNWCVtQHUh+DVswQZg+i5 ++DqaIHz2jUetMo7gWtqGn/wwSopOs87VM1rcALhZL4EsJ+Zy81I/hA32RNnGbuol +NAiZh+0KrtTcc/fPunpd8vRtOwGphM11dKucozUufuiPG2inR3aEqt5yNx54ec/f +J6nryWRYiHEA/rCU9MSBM9cqKFtEmy9/8oxV41/SPxhXjHwDlABWTtFuJ3pf2sOF +ILSYYFwB0ZGvdjE5yAJFBr9efno/L9fafmGk7a3vmVgK2AmUC9VNB5XHw1GjF8OP +aQAXe4md9Bh0jk/D/iyp7e7IWNssul/7XejhabidWgFj6EXc9YxE59+FlhDqyMhn +V6houc+QeUXuwsAKgRJJhJtpv/QSZ5BI3esxHHUt3ayGnvhFElpAc0t7C/EiXKIv +DAFYP2jksBqijM8YtEgPWYzEP5buYxZnf/LK7FDocLsNcdF38UaKBbeF90e7bR8j +SHspG9aJWICu8Yawnh8zuy/vQv+h9gWyGodd2p9lQzlbRXrutbwfmPf7xP6nzT9i +9GcugJxTaZgkCfhhHxFk/nRHS2NAzagKVib1xkUlZJg2hX0fIFUdYteL1GGTvOx5 +m3mTOino4T19z9SEdZYb2OHYh29e/T74bJiLCYdXwevSYHxfZc8pYAf0jp4UnMT2 +f7B0ctX1iXuQ2uZVuxh+U1Mcu+v0gDla1jWh7AhcePSi4xBNUCak0kQip6r5e6Oi +r4MIyMRk/Pc5pzEKo8G6nk26rNvX3aRvECoVfmK7IVdsqZ6IXlt9kOmWx3IeKzrO +J5DxpzW+9oIRZJgPTkc4/XRb0tFmFQYTiChiQ1AJUEiCX0GpkFf7cq61aLGYtWyn +vL2lmQhljzjrDo15hKErvk7eBZW7GW/6j/m/PfRdcBI4ceuP9zWQXnDOd9zmaE4b +q3bJ+IbbyVZA2WwyzN7umCKWghsiPMAolxEnYM9JRf8BcqeqQiwVZlfO5KFuN6Ze +le4= +-----END ENCRYPTED PRIVATE KEY----- diff --git a/assets/php/vendor/react/socket/phpunit.xml.dist b/assets/php/vendor/react/socket/phpunit.xml.dist new file mode 100644 index 0000000..13d3fab --- /dev/null +++ b/assets/php/vendor/react/socket/phpunit.xml.dist @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<phpunit backupGlobals="false" + backupStaticAttributes="false" + colors="true" + convertErrorsToExceptions="true" + convertNoticesToExceptions="true" + convertWarningsToExceptions="true" + processIsolation="false" + stopOnFailure="false" + syntaxCheck="false" + bootstrap="vendor/autoload.php" +> + <testsuites> + <testsuite name="React Test Suite"> + <directory>./tests/</directory> + </testsuite> + </testsuites> + + <filter> + <whitelist> + <directory>./src/</directory> + </whitelist> + </filter> +</phpunit> diff --git a/assets/php/vendor/react/socket/src/Connection.php b/assets/php/vendor/react/socket/src/Connection.php new file mode 100644 index 0000000..c6267cc --- /dev/null +++ b/assets/php/vendor/react/socket/src/Connection.php @@ -0,0 +1,178 @@ +<?php + +namespace React\Socket; + +use Evenement\EventEmitter; +use React\EventLoop\LoopInterface; +use React\Stream\DuplexResourceStream; +use React\Stream\Util; +use React\Stream\WritableResourceStream; +use React\Stream\WritableStreamInterface; + +/** + * The actual connection implementation for ConnectionInterface + * + * This class should only be used internally, see ConnectionInterface instead. + * + * @see ConnectionInterface + * @internal + */ +class Connection extends EventEmitter implements ConnectionInterface +{ + /** + * Internal flag whether this is a Unix domain socket (UDS) connection + * + * @internal + */ + public $unix = false; + + /** + * Internal flag whether encryption has been enabled on this connection + * + * Mostly used by internal StreamEncryption so that connection returns + * `tls://` scheme for encrypted connections instead of `tcp://`. + * + * @internal + */ + public $encryptionEnabled = false; + + /** @internal */ + public $stream; + + private $input; + + public function __construct($resource, LoopInterface $loop) + { + // PHP < 5.6.8 suffers from a buffer indicator bug on secure TLS connections + // as a work-around we always read the complete buffer until its end. + // The buffer size is limited due to TCP/IP buffers anyway, so this + // should not affect usage otherwise. + // See https://bugs.php.net/bug.php?id=65137 + // https://bugs.php.net/bug.php?id=41631 + // https://github.com/reactphp/socket-client/issues/24 + $clearCompleteBuffer = PHP_VERSION_ID < 50608; + + // PHP < 7.1.4 (and PHP < 7.0.18) suffers from a bug when writing big + // chunks of data over TLS streams at once. + // We try to work around this by limiting the write chunk size to 8192 + // bytes for older PHP versions only. + // This is only a work-around and has a noticable performance penalty on + // affected versions. Please update your PHP version. + // This applies to all streams because TLS may be enabled later on. + // See https://github.com/reactphp/socket/issues/105 + $limitWriteChunks = (PHP_VERSION_ID < 70018 || (PHP_VERSION_ID >= 70100 && PHP_VERSION_ID < 70104)); + + $this->input = new DuplexResourceStream( + $resource, + $loop, + $clearCompleteBuffer ? -1 : null, + new WritableResourceStream($resource, $loop, null, $limitWriteChunks ? 8192 : null) + ); + + $this->stream = $resource; + + Util::forwardEvents($this->input, $this, array('data', 'end', 'error', 'close', 'pipe', 'drain')); + + $this->input->on('close', array($this, 'close')); + } + + public function isReadable() + { + return $this->input->isReadable(); + } + + public function isWritable() + { + return $this->input->isWritable(); + } + + public function pause() + { + $this->input->pause(); + } + + public function resume() + { + $this->input->resume(); + } + + public function pipe(WritableStreamInterface $dest, array $options = array()) + { + return $this->input->pipe($dest, $options); + } + + public function write($data) + { + return $this->input->write($data); + } + + public function end($data = null) + { + $this->input->end($data); + } + + public function close() + { + $this->input->close(); + $this->handleClose(); + $this->removeAllListeners(); + } + + public function handleClose() + { + if (!is_resource($this->stream)) { + return; + } + + // Try to cleanly shut down socket and ignore any errors in case other + // side already closed. Shutting down may return to blocking mode on + // some legacy versions, so reset to non-blocking just in case before + // continuing to close the socket resource. + // Underlying Stream implementation will take care of closing file + // handle, so we otherwise keep this open here. + @stream_socket_shutdown($this->stream, STREAM_SHUT_RDWR); + stream_set_blocking($this->stream, false); + } + + public function getRemoteAddress() + { + return $this->parseAddress(@stream_socket_get_name($this->stream, true)); + } + + public function getLocalAddress() + { + return $this->parseAddress(@stream_socket_get_name($this->stream, false)); + } + + private function parseAddress($address) + { + if ($address === false) { + return null; + } + + if ($this->unix) { + // remove trailing colon from address for HHVM < 3.19: https://3v4l.org/5C1lo + // note that technically ":" is a valid address, so keep this in place otherwise + if (substr($address, -1) === ':' && defined('HHVM_VERSION_ID') && HHVM_VERSION_ID < 31900) { + $address = (string)substr($address, 0, -1); + } + + // work around unknown addresses should return null value: https://3v4l.org/5C1lo and https://bugs.php.net/bug.php?id=74556 + // PHP uses "\0" string and HHVM uses empty string (colon removed above) + if ($address === '' || $address[0] === "\x00" ) { + return null; + } + + return 'unix://' . $address; + } + + // check if this is an IPv6 address which includes multiple colons but no square brackets + $pos = strrpos($address, ':'); + if ($pos !== false && strpos($address, ':') < $pos && substr($address, 0, 1) !== '[') { + $port = substr($address, $pos + 1); + $address = '[' . substr($address, 0, $pos) . ']:' . $port; + } + + return ($this->encryptionEnabled ? 'tls' : 'tcp') . '://' . $address; + } +} diff --git a/assets/php/vendor/react/socket/src/ConnectionInterface.php b/assets/php/vendor/react/socket/src/ConnectionInterface.php new file mode 100644 index 0000000..64613b5 --- /dev/null +++ b/assets/php/vendor/react/socket/src/ConnectionInterface.php @@ -0,0 +1,119 @@ +<?php + +namespace React\Socket; + +use React\Stream\DuplexStreamInterface; + +/** + * Any incoming and outgoing connection is represented by this interface, + * such as a normal TCP/IP connection. + * + * An incoming or outgoing connection is a duplex stream (both readable and + * writable) that implements React's + * [`DuplexStreamInterface`](https://github.com/reactphp/stream#duplexstreaminterface). + * It contains additional properties for the local and remote address (client IP) + * where this connection has been established to/from. + * + * Most commonly, instances implementing this `ConnectionInterface` are emitted + * by all classes implementing the [`ServerInterface`](#serverinterface) and + * used by all classes implementing the [`ConnectorInterface`](#connectorinterface). + * + * Because the `ConnectionInterface` implements the underlying + * [`DuplexStreamInterface`](https://github.com/reactphp/stream#duplexstreaminterface) + * you can use any of its events and methods as usual: + * + * ```php + * $connection->on('data', function ($chunk) { + * echo $chunk; + * }); + * + * $connection->on('end', function () { + * echo 'ended'; + * }); + * + * $connection->on('error', function (Exception $e) { + * echo 'error: ' . $e->getMessage(); + * }); + * + * $connection->on('close', function () { + * echo 'closed'; + * }); + * + * $connection->write($data); + * $connection->end($data = null); + * $connection->close(); + * // … + * ``` + * + * For more details, see the + * [`DuplexStreamInterface`](https://github.com/reactphp/stream#duplexstreaminterface). + * + * @see DuplexStreamInterface + * @see ServerInterface + * @see ConnectorInterface + */ +interface ConnectionInterface extends DuplexStreamInterface +{ + /** + * Returns the full remote address (URI) where this connection has been established with + * + * ```php + * $address = $connection->getRemoteAddress(); + * echo 'Connection with ' . $address . PHP_EOL; + * ``` + * + * If the remote address can not be determined or is unknown at this time (such as + * after the connection has been closed), it MAY return a `NULL` value instead. + * + * Otherwise, it will return the full address (URI) as a string value, such + * as `tcp://127.0.0.1:8080`, `tcp://[::1]:80`, `tls://127.0.0.1:443`, + * `unix://example.sock` or `unix:///path/to/example.sock`. + * Note that individual URI components are application specific and depend + * on the underlying transport protocol. + * + * If this is a TCP/IP based connection and you only want the remote IP, you may + * use something like this: + * + * ```php + * $address = $connection->getRemoteAddress(); + * $ip = trim(parse_url($address, PHP_URL_HOST), '[]'); + * echo 'Connection with ' . $ip . PHP_EOL; + * ``` + * + * @return ?string remote address (URI) or null if unknown + */ + public function getRemoteAddress(); + + /** + * Returns the full local address (full URI with scheme, IP and port) where this connection has been established with + * + * ```php + * $address = $connection->getLocalAddress(); + * echo 'Connection with ' . $address . PHP_EOL; + * ``` + * + * If the local address can not be determined or is unknown at this time (such as + * after the connection has been closed), it MAY return a `NULL` value instead. + * + * Otherwise, it will return the full address (URI) as a string value, such + * as `tcp://127.0.0.1:8080`, `tcp://[::1]:80`, `tls://127.0.0.1:443`, + * `unix://example.sock` or `unix:///path/to/example.sock`. + * Note that individual URI components are application specific and depend + * on the underlying transport protocol. + * + * This method complements the [`getRemoteAddress()`](#getremoteaddress) method, + * so they should not be confused. + * + * If your `TcpServer` instance is listening on multiple interfaces (e.g. using + * the address `0.0.0.0`), you can use this method to find out which interface + * actually accepted this connection (such as a public or local interface). + * + * If your system has multiple interfaces (e.g. a WAN and a LAN interface), + * you can use this method to find out which interface was actually + * used for this connection. + * + * @return ?string local address (URI) or null if unknown + * @see self::getRemoteAddress() + */ + public function getLocalAddress(); +} diff --git a/assets/php/vendor/react/socket/src/Connector.php b/assets/php/vendor/react/socket/src/Connector.php new file mode 100644 index 0000000..75276bc --- /dev/null +++ b/assets/php/vendor/react/socket/src/Connector.php @@ -0,0 +1,136 @@ +<?php + +namespace React\Socket; + +use React\Dns\Config\Config; +use React\Dns\Resolver\Factory; +use React\Dns\Resolver\Resolver; +use React\EventLoop\LoopInterface; +use React\Promise; +use RuntimeException; + +/** + * The `Connector` class is the main class in this package that implements the + * `ConnectorInterface` and allows you to create streaming connections. + * + * You can use this connector to create any kind of streaming connections, such + * as plaintext TCP/IP, secure TLS or local Unix connection streams. + * + * Under the hood, the `Connector` is implemented as a *higher-level facade* + * or the lower-level connectors implemented in this package. This means it + * also shares all of their features and implementation details. + * If you want to typehint in your higher-level protocol implementation, you SHOULD + * use the generic [`ConnectorInterface`](#connectorinterface) instead. + * + * @see ConnectorInterface for the base interface + */ +final class Connector implements ConnectorInterface +{ + private $connectors = array(); + + public function __construct(LoopInterface $loop, array $options = array()) + { + // apply default options if not explicitly given + $options += array( + 'tcp' => true, + 'tls' => true, + 'unix' => true, + + 'dns' => true, + 'timeout' => true, + ); + + if ($options['timeout'] === true) { + $options['timeout'] = (float)ini_get("default_socket_timeout"); + } + + if ($options['tcp'] instanceof ConnectorInterface) { + $tcp = $options['tcp']; + } else { + $tcp = new TcpConnector( + $loop, + is_array($options['tcp']) ? $options['tcp'] : array() + ); + } + + if ($options['dns'] !== false) { + if ($options['dns'] instanceof Resolver) { + $resolver = $options['dns']; + } else { + if ($options['dns'] !== true) { + $server = $options['dns']; + } else { + // try to load nameservers from system config or default to Google's public DNS + $config = Config::loadSystemConfigBlocking(); + $server = $config->nameservers ? reset($config->nameservers) : '8.8.8.8'; + } + + $factory = new Factory(); + $resolver = $factory->create( + $server, + $loop + ); + } + + $tcp = new DnsConnector($tcp, $resolver); + } + + if ($options['tcp'] !== false) { + $options['tcp'] = $tcp; + + if ($options['timeout'] !== false) { + $options['tcp'] = new TimeoutConnector( + $options['tcp'], + $options['timeout'], + $loop + ); + } + + $this->connectors['tcp'] = $options['tcp']; + } + + if ($options['tls'] !== false) { + if (!$options['tls'] instanceof ConnectorInterface) { + $options['tls'] = new SecureConnector( + $tcp, + $loop, + is_array($options['tls']) ? $options['tls'] : array() + ); + } + + if ($options['timeout'] !== false) { + $options['tls'] = new TimeoutConnector( + $options['tls'], + $options['timeout'], + $loop + ); + } + + $this->connectors['tls'] = $options['tls']; + } + + if ($options['unix'] !== false) { + if (!$options['unix'] instanceof ConnectorInterface) { + $options['unix'] = new UnixConnector($loop); + } + $this->connectors['unix'] = $options['unix']; + } + } + + public function connect($uri) + { + $scheme = 'tcp'; + if (strpos($uri, '://') !== false) { + $scheme = (string)substr($uri, 0, strpos($uri, '://')); + } + + if (!isset($this->connectors[$scheme])) { + return Promise\reject(new RuntimeException( + 'No connector available for URI scheme "' . $scheme . '"' + )); + } + + return $this->connectors[$scheme]->connect($uri); + } +} + diff --git a/assets/php/vendor/react/socket/src/ConnectorInterface.php b/assets/php/vendor/react/socket/src/ConnectorInterface.php new file mode 100644 index 0000000..196d01a --- /dev/null +++ b/assets/php/vendor/react/socket/src/ConnectorInterface.php @@ -0,0 +1,58 @@ +<?php + +namespace React\Socket; + +/** + * The `ConnectorInterface` is responsible for providing an interface for + * establishing streaming connections, such as a normal TCP/IP connection. + * + * This is the main interface defined in this package and it is used throughout + * React's vast ecosystem. + * + * Most higher-level components (such as HTTP, database or other networking + * service clients) accept an instance implementing this interface to create their + * TCP/IP connection to the underlying networking service. + * This is usually done via dependency injection, so it's fairly simple to actually + * swap this implementation against any other implementation of this interface. + * + * The interface only offers a single `connect()` method. + * + * @see ConnectionInterface + */ +interface ConnectorInterface +{ + /** + * Creates a streaming connection to the given remote address + * + * If returns a Promise which either fulfills with a stream implementing + * `ConnectionInterface` on success or rejects with an `Exception` if the + * connection is not successful. + * + * ```php + * $connector->connect('google.com:443')->then( + * function (ConnectionInterface $connection) { + * // connection successfully established + * }, + * function (Exception $error) { + * // failed to connect due to $error + * } + * ); + * ``` + * + * The returned Promise MUST be implemented in such a way that it can be + * cancelled when it is still pending. Cancelling a pending promise MUST + * reject its value with an Exception. It SHOULD clean up any underlying + * resources and references as applicable. + * + * ```php + * $promise = $connector->connect($uri); + * + * $promise->cancel(); + * ``` + * + * @param string $uri + * @return \React\Promise\PromiseInterface resolves with a stream implementing ConnectionInterface on success or rejects with an Exception on error + * @see ConnectionInterface + */ + public function connect($uri); +} diff --git a/assets/php/vendor/react/socket/src/DnsConnector.php b/assets/php/vendor/react/socket/src/DnsConnector.php new file mode 100644 index 0000000..90170e5 --- /dev/null +++ b/assets/php/vendor/react/socket/src/DnsConnector.php @@ -0,0 +1,111 @@ +<?php + +namespace React\Socket; + +use React\Dns\Resolver\Resolver; +use React\Promise; +use React\Promise\CancellablePromiseInterface; +use InvalidArgumentException; +use RuntimeException; + +final class DnsConnector implements ConnectorInterface +{ + private $connector; + private $resolver; + + public function __construct(ConnectorInterface $connector, Resolver $resolver) + { + $this->connector = $connector; + $this->resolver = $resolver; + } + + public function connect($uri) + { + if (strpos($uri, '://') === false) { + $parts = parse_url('tcp://' . $uri); + unset($parts['scheme']); + } else { + $parts = parse_url($uri); + } + + if (!$parts || !isset($parts['host'])) { + return Promise\reject(new InvalidArgumentException('Given URI "' . $uri . '" is invalid')); + } + + $host = trim($parts['host'], '[]'); + $connector = $this->connector; + + return $this + ->resolveHostname($host) + ->then(function ($ip) use ($connector, $host, $parts) { + $uri = ''; + + // prepend original scheme if known + if (isset($parts['scheme'])) { + $uri .= $parts['scheme'] . '://'; + } + + if (strpos($ip, ':') !== false) { + // enclose IPv6 addresses in square brackets before appending port + $uri .= '[' . $ip . ']'; + } else { + $uri .= $ip; + } + + // append original port if known + if (isset($parts['port'])) { + $uri .= ':' . $parts['port']; + } + + // append orignal path if known + if (isset($parts['path'])) { + $uri .= $parts['path']; + } + + // append original query if known + if (isset($parts['query'])) { + $uri .= '?' . $parts['query']; + } + + // append original hostname as query if resolved via DNS and if + // destination URI does not contain "hostname" query param already + $args = array(); + parse_str(isset($parts['query']) ? $parts['query'] : '', $args); + if ($host !== $ip && !isset($args['hostname'])) { + $uri .= (isset($parts['query']) ? '&' : '?') . 'hostname=' . rawurlencode($host); + } + + // append original fragment if known + if (isset($parts['fragment'])) { + $uri .= '#' . $parts['fragment']; + } + + return $connector->connect($uri); + }); + } + + private function resolveHostname($host) + { + if (false !== filter_var($host, FILTER_VALIDATE_IP)) { + return Promise\resolve($host); + } + + $promise = $this->resolver->resolve($host); + + return new Promise\Promise( + function ($resolve, $reject) use ($promise) { + // resolve/reject with result of DNS lookup + $promise->then($resolve, $reject); + }, + function ($_, $reject) use ($promise) { + // cancellation should reject connection attempt + $reject(new RuntimeException('Connection attempt cancelled during DNS lookup')); + + // (try to) cancel pending DNS lookup + if ($promise instanceof CancellablePromiseInterface) { + $promise->cancel(); + } + } + ); + } +} diff --git a/assets/php/vendor/react/socket/src/FixedUriConnector.php b/assets/php/vendor/react/socket/src/FixedUriConnector.php new file mode 100644 index 0000000..057bcdf --- /dev/null +++ b/assets/php/vendor/react/socket/src/FixedUriConnector.php @@ -0,0 +1,41 @@ +<?php + +namespace React\Socket; + +/** + * Decorates an existing Connector to always use a fixed, preconfigured URI + * + * This can be useful for consumers that do not support certain URIs, such as + * when you want to explicitly connect to a Unix domain socket (UDS) path + * instead of connecting to a default address assumed by an higher-level API: + * + * ```php + * $connector = new FixedUriConnector( + * 'unix:///var/run/docker.sock', + * new UnixConnector($loop) + * ); + * + * // destination will be ignored, actually connects to Unix domain socket + * $promise = $connector->connect('localhost:80'); + * ``` + */ +class FixedUriConnector implements ConnectorInterface +{ + private $uri; + private $connector; + + /** + * @param string $uri + * @param ConnectorInterface $connector + */ + public function __construct($uri, ConnectorInterface $connector) + { + $this->uri = $uri; + $this->connector = $connector; + } + + public function connect($_) + { + return $this->connector->connect($this->uri); + } +} diff --git a/assets/php/vendor/react/socket/src/LimitingServer.php b/assets/php/vendor/react/socket/src/LimitingServer.php new file mode 100644 index 0000000..c7874ee --- /dev/null +++ b/assets/php/vendor/react/socket/src/LimitingServer.php @@ -0,0 +1,203 @@ +<?php + +namespace React\Socket; + +use Evenement\EventEmitter; +use Exception; +use OverflowException; + +/** + * The `LimitingServer` decorator wraps a given `ServerInterface` and is responsible + * for limiting and keeping track of open connections to this server instance. + * + * Whenever the underlying server emits a `connection` event, it will check its + * limits and then either + * - keep track of this connection by adding it to the list of + * open connections and then forward the `connection` event + * - or reject (close) the connection when its limits are exceeded and will + * forward an `error` event instead. + * + * Whenever a connection closes, it will remove this connection from the list of + * open connections. + * + * ```php + * $server = new LimitingServer($server, 100); + * $server->on('connection', function (ConnectionInterface $connection) { + * $connection->write('hello there!' . PHP_EOL); + * … + * }); + * ``` + * + * See also the `ServerInterface` for more details. + * + * @see ServerInterface + * @see ConnectionInterface + */ +class LimitingServer extends EventEmitter implements ServerInterface +{ + private $connections = array(); + private $server; + private $limit; + + private $pauseOnLimit = false; + private $autoPaused = false; + private $manuPaused = false; + + /** + * Instantiates a new LimitingServer. + * + * You have to pass a maximum number of open connections to ensure + * the server will automatically reject (close) connections once this limit + * is exceeded. In this case, it will emit an `error` event to inform about + * this and no `connection` event will be emitted. + * + * ```php + * $server = new LimitingServer($server, 100); + * $server->on('connection', function (ConnectionInterface $connection) { + * $connection->write('hello there!' . PHP_EOL); + * … + * }); + * ``` + * + * You MAY pass a `null` limit in order to put no limit on the number of + * open connections and keep accepting new connection until you run out of + * operating system resources (such as open file handles). This may be + * useful if you do not want to take care of applying a limit but still want + * to use the `getConnections()` method. + * + * You can optionally configure the server to pause accepting new + * connections once the connection limit is reached. In this case, it will + * pause the underlying server and no longer process any new connections at + * all, thus also no longer closing any excessive connections. + * The underlying operating system is responsible for keeping a backlog of + * pending connections until its limit is reached, at which point it will + * start rejecting further connections. + * Once the server is below the connection limit, it will continue consuming + * connections from the backlog and will process any outstanding data on + * each connection. + * This mode may be useful for some protocols that are designed to wait for + * a response message (such as HTTP), but may be less useful for other + * protocols that demand immediate responses (such as a "welcome" message in + * an interactive chat). + * + * ```php + * $server = new LimitingServer($server, 100, true); + * $server->on('connection', function (ConnectionInterface $connection) { + * $connection->write('hello there!' . PHP_EOL); + * … + * }); + * ``` + * + * @param ServerInterface $server + * @param int|null $connectionLimit + * @param bool $pauseOnLimit + */ + public function __construct(ServerInterface $server, $connectionLimit, $pauseOnLimit = false) + { + $this->server = $server; + $this->limit = $connectionLimit; + if ($connectionLimit !== null) { + $this->pauseOnLimit = $pauseOnLimit; + } + + $this->server->on('connection', array($this, 'handleConnection')); + $this->server->on('error', array($this, 'handleError')); + } + + /** + * Returns an array with all currently active connections + * + * ```php + * foreach ($server->getConnection() as $connection) { + * $connection->write('Hi!'); + * } + * ``` + * + * @return ConnectionInterface[] + */ + public function getConnections() + { + return $this->connections; + } + + public function getAddress() + { + return $this->server->getAddress(); + } + + public function pause() + { + if (!$this->manuPaused) { + $this->manuPaused = true; + + if (!$this->autoPaused) { + $this->server->pause(); + } + } + } + + public function resume() + { + if ($this->manuPaused) { + $this->manuPaused = false; + + if (!$this->autoPaused) { + $this->server->resume(); + } + } + } + + public function close() + { + $this->server->close(); + } + + /** @internal */ + public function handleConnection(ConnectionInterface $connection) + { + // close connection if limit exceeded + if ($this->limit !== null && count($this->connections) >= $this->limit) { + $this->handleError(new OverflowException('Connection closed because server reached connection limit')); + $connection->close(); + return; + } + + $this->connections[] = $connection; + $that = $this; + $connection->on('close', function () use ($that, $connection) { + $that->handleDisconnection($connection); + }); + + // pause accepting new connections if limit exceeded + if ($this->pauseOnLimit && !$this->autoPaused && count($this->connections) >= $this->limit) { + $this->autoPaused = true; + + if (!$this->manuPaused) { + $this->server->pause(); + } + } + + $this->emit('connection', array($connection)); + } + + /** @internal */ + public function handleDisconnection(ConnectionInterface $connection) + { + unset($this->connections[array_search($connection, $this->connections)]); + + // continue accepting new connection if below limit + if ($this->autoPaused && count($this->connections) < $this->limit) { + $this->autoPaused = false; + + if (!$this->manuPaused) { + $this->server->resume(); + } + } + } + + /** @internal */ + public function handleError(Exception $error) + { + $this->emit('error', array($error)); + } +} diff --git a/assets/php/vendor/react/socket/src/SecureConnector.php b/assets/php/vendor/react/socket/src/SecureConnector.php new file mode 100644 index 0000000..f04183d --- /dev/null +++ b/assets/php/vendor/react/socket/src/SecureConnector.php @@ -0,0 +1,64 @@ +<?php + +namespace React\Socket; + +use React\EventLoop\LoopInterface; +use React\Promise; +use BadMethodCallException; +use InvalidArgumentException; +use UnexpectedValueException; + +final class SecureConnector implements ConnectorInterface +{ + private $connector; + private $streamEncryption; + private $context; + + public function __construct(ConnectorInterface $connector, LoopInterface $loop, array $context = array()) + { + $this->connector = $connector; + $this->streamEncryption = new StreamEncryption($loop, false); + $this->context = $context; + } + + public function connect($uri) + { + if (!function_exists('stream_socket_enable_crypto')) { + return Promise\reject(new BadMethodCallException('Encryption not supported on your platform (HHVM < 3.8?)')); // @codeCoverageIgnore + } + + if (strpos($uri, '://') === false) { + $uri = 'tls://' . $uri; + } + + $parts = parse_url($uri); + if (!$parts || !isset($parts['scheme']) || $parts['scheme'] !== 'tls') { + return Promise\reject(new InvalidArgumentException('Given URI "' . $uri . '" is invalid')); + } + + $uri = str_replace('tls://', '', $uri); + $context = $this->context; + + $encryption = $this->streamEncryption; + return $this->connector->connect($uri)->then(function (ConnectionInterface $connection) use ($context, $encryption) { + // (unencrypted) TCP/IP connection succeeded + + if (!$connection instanceof Connection) { + $connection->close(); + throw new UnexpectedValueException('Base connector does not use internal Connection class exposing stream resource'); + } + + // set required SSL/TLS context options + foreach ($context as $name => $value) { + stream_context_set_option($connection->stream, 'ssl', $name, $value); + } + + // try to enable encryption + return $encryption->enable($connection)->then(null, function ($error) use ($connection) { + // establishing encryption failed => close invalid connection and return error + $connection->close(); + throw $error; + }); + }); + } +} diff --git a/assets/php/vendor/react/socket/src/SecureServer.php b/assets/php/vendor/react/socket/src/SecureServer.php new file mode 100644 index 0000000..302ae93 --- /dev/null +++ b/assets/php/vendor/react/socket/src/SecureServer.php @@ -0,0 +1,192 @@ +<?php + +namespace React\Socket; + +use Evenement\EventEmitter; +use React\EventLoop\LoopInterface; +use BadMethodCallException; +use UnexpectedValueException; + +/** + * The `SecureServer` class implements the `ServerInterface` and is responsible + * for providing a secure TLS (formerly known as SSL) server. + * + * It does so by wrapping a `TcpServer` instance which waits for plaintext + * TCP/IP connections and then performs a TLS handshake for each connection. + * + * ```php + * $server = new TcpServer(8000, $loop); + * $server = new SecureServer($server, $loop, array( + * // tls context options here… + * )); + * ``` + * + * Whenever a client completes the TLS handshake, it will emit a `connection` event + * with a connection instance implementing [`ConnectionInterface`](#connectioninterface): + * + * ```php + * $server->on('connection', function (ConnectionInterface $connection) { + * echo 'Secure connection from' . $connection->getRemoteAddress() . PHP_EOL; + * + * $connection->write('hello there!' . PHP_EOL); + * … + * }); + * ``` + * + * Whenever a client fails to perform a successful TLS handshake, it will emit an + * `error` event and then close the underlying TCP/IP connection: + * + * ```php + * $server->on('error', function (Exception $e) { + * echo 'Error' . $e->getMessage() . PHP_EOL; + * }); + * ``` + * + * See also the `ServerInterface` for more details. + * + * Note that the `SecureServer` class is a concrete implementation for TLS sockets. + * If you want to typehint in your higher-level protocol implementation, you SHOULD + * use the generic `ServerInterface` instead. + * + * @see ServerInterface + * @see ConnectionInterface + */ +final class SecureServer extends EventEmitter implements ServerInterface +{ + private $tcp; + private $encryption; + private $context; + + /** + * Creates a secure TLS server and starts waiting for incoming connections + * + * It does so by wrapping a `TcpServer` instance which waits for plaintext + * TCP/IP connections and then performs a TLS handshake for each connection. + * It thus requires valid [TLS context options], + * which in its most basic form may look something like this if you're using a + * PEM encoded certificate file: + * + * ```php + * $server = new TcpServer(8000, $loop); + * $server = new SecureServer($server, $loop, array( + * 'local_cert' => 'server.pem' + * )); + * ``` + * + * Note that the certificate file will not be loaded on instantiation but when an + * incoming connection initializes its TLS context. + * This implies that any invalid certificate file paths or contents will only cause + * an `error` event at a later time. + * + * If your private key is encrypted with a passphrase, you have to specify it + * like this: + * + * ```php + * $server = new TcpServer(8000, $loop); + * $server = new SecureServer($server, $loop, array( + * 'local_cert' => 'server.pem', + * 'passphrase' => 'secret' + * )); + * ``` + * + * Note that available [TLS context options], + * their defaults and effects of changing these may vary depending on your system + * and/or PHP version. + * Passing unknown context options has no effect. + * + * Advanced usage: Despite allowing any `ServerInterface` as first parameter, + * you SHOULD pass a `TcpServer` instance as first parameter, unless you + * know what you're doing. + * Internally, the `SecureServer` has to set the required TLS context options on + * the underlying stream resources. + * These resources are not exposed through any of the interfaces defined in this + * package, but only through the internal `Connection` class. + * The `TcpServer` class is guaranteed to emit connections that implement + * the `ConnectionInterface` and uses the internal `Connection` class in order to + * expose these underlying resources. + * If you use a custom `ServerInterface` and its `connection` event does not + * meet this requirement, the `SecureServer` will emit an `error` event and + * then close the underlying connection. + * + * @param ServerInterface|TcpServer $tcp + * @param LoopInterface $loop + * @param array $context + * @throws BadMethodCallException for legacy HHVM < 3.8 due to lack of support + * @see TcpServer + * @link http://php.net/manual/en/context.ssl.php for TLS context options + */ + public function __construct(ServerInterface $tcp, LoopInterface $loop, array $context) + { + if (!function_exists('stream_socket_enable_crypto')) { + throw new BadMethodCallException('Encryption not supported on your platform (HHVM < 3.8?)'); // @codeCoverageIgnore + } + + // default to empty passphrase to suppress blocking passphrase prompt + $context += array( + 'passphrase' => '' + ); + + $this->tcp = $tcp; + $this->encryption = new StreamEncryption($loop); + $this->context = $context; + + $that = $this; + $this->tcp->on('connection', function ($connection) use ($that) { + $that->handleConnection($connection); + }); + $this->tcp->on('error', function ($error) use ($that) { + $that->emit('error', array($error)); + }); + } + + public function getAddress() + { + $address = $this->tcp->getAddress(); + if ($address === null) { + return null; + } + + return str_replace('tcp://' , 'tls://', $address); + } + + public function pause() + { + $this->tcp->pause(); + } + + public function resume() + { + $this->tcp->resume(); + } + + public function close() + { + return $this->tcp->close(); + } + + /** @internal */ + public function handleConnection(ConnectionInterface $connection) + { + if (!$connection instanceof Connection) { + $this->emit('error', array(new UnexpectedValueException('Base server does not use internal Connection class exposing stream resource'))); + $connection->end(); + return; + } + + foreach ($this->context as $name => $value) { + stream_context_set_option($connection->stream, 'ssl', $name, $value); + } + + $that = $this; + + $this->encryption->enable($connection)->then( + function ($conn) use ($that) { + $that->emit('connection', array($conn)); + }, + function ($error) use ($that, $connection) { + $that->emit('error', array($error)); + $connection->end(); + } + ); + } +} diff --git a/assets/php/vendor/react/socket/src/Server.php b/assets/php/vendor/react/socket/src/Server.php new file mode 100644 index 0000000..72712e4 --- /dev/null +++ b/assets/php/vendor/react/socket/src/Server.php @@ -0,0 +1,73 @@ +<?php + +namespace React\Socket; + +use Evenement\EventEmitter; +use React\EventLoop\LoopInterface; +use Exception; + +final class Server extends EventEmitter implements ServerInterface +{ + private $server; + + public function __construct($uri, LoopInterface $loop, array $context = array()) + { + // sanitize TCP context options if not properly wrapped + if ($context && (!isset($context['tcp']) && !isset($context['tls']) && !isset($context['unix']))) { + $context = array('tcp' => $context); + } + + // apply default options if not explicitly given + $context += array( + 'tcp' => array(), + 'tls' => array(), + 'unix' => array() + ); + + $scheme = 'tcp'; + $pos = strpos($uri, '://'); + if ($pos !== false) { + $scheme = substr($uri, 0, $pos); + } + + if ($scheme === 'unix') { + $server = new UnixServer($uri, $loop, $context['unix']); + } else { + $server = new TcpServer(str_replace('tls://', '', $uri), $loop, $context['tcp']); + + if ($scheme === 'tls') { + $server = new SecureServer($server, $loop, $context['tls']); + } + } + + $this->server = $server; + + $that = $this; + $server->on('connection', function (ConnectionInterface $conn) use ($that) { + $that->emit('connection', array($conn)); + }); + $server->on('error', function (Exception $error) use ($that) { + $that->emit('error', array($error)); + }); + } + + public function getAddress() + { + return $this->server->getAddress(); + } + + public function pause() + { + $this->server->pause(); + } + + public function resume() + { + $this->server->resume(); + } + + public function close() + { + $this->server->close(); + } +} diff --git a/assets/php/vendor/react/socket/src/ServerInterface.php b/assets/php/vendor/react/socket/src/ServerInterface.php new file mode 100644 index 0000000..5319678 --- /dev/null +++ b/assets/php/vendor/react/socket/src/ServerInterface.php @@ -0,0 +1,151 @@ +<?php + +namespace React\Socket; + +use Evenement\EventEmitterInterface; + +/** + * The `ServerInterface` is responsible for providing an interface for accepting + * incoming streaming connections, such as a normal TCP/IP connection. + * + * Most higher-level components (such as a HTTP server) accept an instance + * implementing this interface to accept incoming streaming connections. + * This is usually done via dependency injection, so it's fairly simple to actually + * swap this implementation against any other implementation of this interface. + * This means that you SHOULD typehint against this interface instead of a concrete + * implementation of this interface. + * + * Besides defining a few methods, this interface also implements the + * `EventEmitterInterface` which allows you to react to certain events: + * + * connection event: + * The `connection` event will be emitted whenever a new connection has been + * established, i.e. a new client connects to this server socket: + * + * ```php + * $server->on('connection', function (ConnectionInterface $connection) { + * echo 'new connection' . PHP_EOL; + * }); + * ``` + * + * See also the `ConnectionInterface` for more details about handling the + * incoming connection. + * + * error event: + * The `error` event will be emitted whenever there's an error accepting a new + * connection from a client. + * + * ```php + * $server->on('error', function (Exception $e) { + * echo 'error: ' . $e->getMessage() . PHP_EOL; + * }); + * ``` + * + * Note that this is not a fatal error event, i.e. the server keeps listening for + * new connections even after this event. + * + * @see ConnectionInterface + */ +interface ServerInterface extends EventEmitterInterface +{ + /** + * Returns the full address (URI) this server is currently listening on + * + * ```php + * $address = $server->getAddress(); + * echo 'Server listening on ' . $address . PHP_EOL; + * ``` + * + * If the address can not be determined or is unknown at this time (such as + * after the socket has been closed), it MAY return a `NULL` value instead. + * + * Otherwise, it will return the full address (URI) as a string value, such + * as `tcp://127.0.0.1:8080`, `tcp://[::1]:80` or `tls://127.0.0.1:443`. + * Note that individual URI components are application specific and depend + * on the underlying transport protocol. + * + * If this is a TCP/IP based server and you only want the local port, you may + * use something like this: + * + * ```php + * $address = $server->getAddress(); + * $port = parse_url($address, PHP_URL_PORT); + * echo 'Server listening on port ' . $port . PHP_EOL; + * ``` + * + * @return ?string the full listening address (URI) or NULL if it is unknown (not applicable to this server socket or already closed) + */ + public function getAddress(); + + /** + * Pauses accepting new incoming connections. + * + * Removes the socket resource from the EventLoop and thus stop accepting + * new connections. Note that the listening socket stays active and is not + * closed. + * + * This means that new incoming connections will stay pending in the + * operating system backlog until its configurable backlog is filled. + * Once the backlog is filled, the operating system may reject further + * incoming connections until the backlog is drained again by resuming + * to accept new connections. + * + * Once the server is paused, no futher `connection` events SHOULD + * be emitted. + * + * ```php + * $server->pause(); + * + * $server->on('connection', assertShouldNeverCalled()); + * ``` + * + * This method is advisory-only, though generally not recommended, the + * server MAY continue emitting `connection` events. + * + * Unless otherwise noted, a successfully opened server SHOULD NOT start + * in paused state. + * + * You can continue processing events by calling `resume()` again. + * + * Note that both methods can be called any number of times, in particular + * calling `pause()` more than once SHOULD NOT have any effect. + * Similarly, calling this after `close()` is a NO-OP. + * + * @see self::resume() + * @return void + */ + public function pause(); + + /** + * Resumes accepting new incoming connections. + * + * Re-attach the socket resource to the EventLoop after a previous `pause()`. + * + * ```php + * $server->pause(); + * + * $loop->addTimer(1.0, function () use ($server) { + * $server->resume(); + * }); + * ``` + * + * Note that both methods can be called any number of times, in particular + * calling `resume()` without a prior `pause()` SHOULD NOT have any effect. + * Similarly, calling this after `close()` is a NO-OP. + * + * @see self::pause() + * @return void + */ + public function resume(); + + /** + * Shuts down this listening socket + * + * This will stop listening for new incoming connections on this socket. + * + * Calling this method more than once on the same instance is a NO-OP. + * + * @return void + */ + public function close(); +} diff --git a/assets/php/vendor/react/socket/src/StreamEncryption.php b/assets/php/vendor/react/socket/src/StreamEncryption.php new file mode 100644 index 0000000..ba5d472 --- /dev/null +++ b/assets/php/vendor/react/socket/src/StreamEncryption.php @@ -0,0 +1,146 @@ +<?php + +namespace React\Socket; + +use React\EventLoop\LoopInterface; +use React\Promise\Deferred; +use RuntimeException; +use UnexpectedValueException; + +/** + * This class is considered internal and its API should not be relied upon + * outside of Socket. + * + * @internal + */ +class StreamEncryption +{ + private $loop; + private $method; + private $server; + + private $errstr; + private $errno; + + public function __construct(LoopInterface $loop, $server = true) + { + $this->loop = $loop; + $this->server = $server; + + // support TLSv1.0+ by default and exclude legacy SSLv2/SSLv3. + // PHP 5.6+ supports bitmasks, legacy PHP only supports predefined + // constants, so apply accordingly below. + // Also, since PHP 5.6.7 up until before PHP 7.2.0 the main constant did + // only support TLSv1.0, so we explicitly apply all versions. + // @link http://php.net/manual/en/migration56.openssl.php#migration56.openssl.crypto-method + // @link https://3v4l.org/plbFn + if ($server) { + $this->method = STREAM_CRYPTO_METHOD_TLS_SERVER; + + if (defined('STREAM_CRYPTO_METHOD_TLSv1_0_SERVER')) { + $this->method |= STREAM_CRYPTO_METHOD_TLSv1_0_SERVER; + } + if (defined('STREAM_CRYPTO_METHOD_TLSv1_1_SERVER')) { + $this->method |= STREAM_CRYPTO_METHOD_TLSv1_1_SERVER; + } + if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_SERVER')) { + $this->method |= STREAM_CRYPTO_METHOD_TLSv1_2_SERVER; + } + } else { + $this->method = STREAM_CRYPTO_METHOD_TLS_CLIENT; + + if (defined('STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT')) { + $this->method |= STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT; + } + if (defined('STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT')) { + $this->method |= STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT; + } + if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) { + $this->method |= STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT; + } + } + } + + public function enable(Connection $stream) + { + return $this->toggle($stream, true); + } + + public function disable(Connection $stream) + { + return $this->toggle($stream, false); + } + + public function toggle(Connection $stream, $toggle) + { + // pause actual stream instance to continue operation on raw stream socket + $stream->pause(); + + // TODO: add write() event to make sure we're not sending any excessive data + + $deferred = new Deferred(function ($_, $reject) use ($toggle) { + // cancelling this leaves this stream in an inconsistent state… + $reject(new RuntimeException('Cancelled toggling encryption ' . $toggle ? 'on' : 'off')); + }); + + // get actual stream socket from stream instance + $socket = $stream->stream; + + // get crypto method from context options or use global setting from constructor + $method = $this->method; + $context = stream_context_get_options($socket); + if (isset($context['ssl']['crypto_method'])) { + $method = $context['ssl']['crypto_method']; + } + + $that = $this; + $toggleCrypto = function () use ($socket, $deferred, $toggle, $method, $that) { + $that->toggleCrypto($socket, $deferred, $toggle, $method); + }; + + $this->loop->addReadStream($socket, $toggleCrypto); + + if (!$this->server) { + $toggleCrypto(); + } + + $loop = $this->loop; + + return $deferred->promise()->then(function () use ($stream, $socket, $loop, $toggle) { + $loop->removeReadStream($socket); + + $stream->encryptionEnabled = $toggle; + $stream->resume(); + + return $stream; + }, function($error) use ($stream, $socket, $loop) { + $loop->removeReadStream($socket); + $stream->resume(); + throw $error; + }); + } + + public function toggleCrypto($socket, Deferred $deferred, $toggle, $method) + { + set_error_handler(array($this, 'handleError')); + $result = stream_socket_enable_crypto($socket, $toggle, $method); + restore_error_handler(); + + if (true === $result) { + $deferred->resolve(); + } else if (false === $result) { + $deferred->reject(new UnexpectedValueException( + sprintf("Unable to complete SSL/TLS handshake: %s", $this->errstr), + $this->errno + )); + } else { + // need more data, will retry + } + } + + public function handleError($errno, $errstr) + { + $this->errstr = str_replace(array("\r", "\n"), ' ', $errstr); + $this->errno = $errno; + } +} diff --git a/assets/php/vendor/react/socket/src/TcpConnector.php b/assets/php/vendor/react/socket/src/TcpConnector.php new file mode 100644 index 0000000..90d7df1 --- /dev/null +++ b/assets/php/vendor/react/socket/src/TcpConnector.php @@ -0,0 +1,122 @@ +<?php + +namespace React\Socket; + +use React\EventLoop\LoopInterface; +use React\Promise; +use InvalidArgumentException; +use RuntimeException; + +final class TcpConnector implements ConnectorInterface +{ + private $loop; + private $context; + + public function __construct(LoopInterface $loop, array $context = array()) + { + $this->loop = $loop; + $this->context = $context; + } + + public function connect($uri) + { + if (strpos($uri, '://') === false) { + $uri = 'tcp://' . $uri; + } + + $parts = parse_url($uri); + if (!$parts || !isset($parts['scheme'], $parts['host'], $parts['port']) || $parts['scheme'] !== 'tcp') { + return Promise\reject(new InvalidArgumentException('Given URI "' . $uri . '" is invalid')); + } + + $ip = trim($parts['host'], '[]'); + if (false === filter_var($ip, FILTER_VALIDATE_IP)) { + return Promise\reject(new InvalidArgumentException('Given URI "' . $ip . '" does not contain a valid host IP')); + } + + // use context given in constructor + $context = array( + 'socket' => $this->context + ); + + // parse arguments from query component of URI + $args = array(); + if (isset($parts['query'])) { + parse_str($parts['query'], $args); + } + + // If an original hostname has been given, use this for TLS setup. + // This can happen due to layers of nested connectors, such as a + // DnsConnector reporting its original hostname. + // These context options are here in case TLS is enabled later on this stream. + // If TLS is not enabled later, this doesn't hurt either. + if (isset($args['hostname'])) { + $context['ssl'] = array( + 'SNI_enabled' => true, + 'peer_name' => $args['hostname'] + ); + + // Legacy PHP < 5.6 ignores peer_name and requires legacy context options instead. + // The SNI_server_name context option has to be set here during construction, + // as legacy PHP ignores any values set later. + if (PHP_VERSION_ID < 50600) { + $context['ssl'] += array( + 'SNI_server_name' => $args['hostname'], + 'CN_match' => $args['hostname'] + ); + } + } + + // latest versions of PHP no longer accept any other URI components and + // HHVM fails to parse URIs with a query but no path, so let's simplify our URI here + $remote = 'tcp://' . $parts['host'] . ':' . $parts['port']; + + $socket = @stream_socket_client( + $remote, + $errno, + $errstr, + 0, + STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT, + stream_context_create($context) + ); + + if (false === $socket) { + return Promise\reject(new RuntimeException( + sprintf("Connection to %s failed: %s", $uri, $errstr), + $errno + )); + } + + stream_set_blocking($socket, 0); + + // wait for connection + + return $this->waitForStreamOnce($socket); + } + + private function waitForStreamOnce($stream) + { + $loop = $this->loop; + + return new Promise\Promise(function ($resolve, $reject) use ($loop, $stream) { + $loop->addWriteStream($stream, function ($stream) use ($loop, $resolve, $reject) { + $loop->removeWriteStream($stream); + + // The following hack looks like the only way to + // detect connection refused errors with PHP's stream sockets. + if (false === stream_socket_get_name($stream, true)) { + fclose($stream); + + $reject(new RuntimeException('Connection refused')); + } else { + $resolve(new Connection($stream, $loop)); + } + }); + }, function () use ($loop, $stream) { + $loop->removeWriteStream($stream); + fclose($stream); + + throw new RuntimeException('Cancelled while waiting for TCP/IP connection to be established'); + }); + } +} diff --git a/assets/php/vendor/react/socket/src/TcpServer.php b/assets/php/vendor/react/socket/src/TcpServer.php new file mode 100644 index 0000000..119e177 --- /dev/null +++ b/assets/php/vendor/react/socket/src/TcpServer.php @@ -0,0 +1,236 @@ +<?php + +namespace React\Socket; + +use Evenement\EventEmitter; +use React\EventLoop\LoopInterface; +use InvalidArgumentException; +use RuntimeException; + +/** + * The `TcpServer` class implements the `ServerInterface` and + * is responsible for accepting plaintext TCP/IP connections. + * + * ```php + * $server = new TcpServer(8080, $loop); + * ``` + * + * Whenever a client connects, it will emit a `connection` event with a connection + * instance implementing `ConnectionInterface`: + * + * ```php + * $server->on('connection', function (ConnectionInterface $connection) { + * echo 'Plaintext connection from ' . $connection->getRemoteAddress() . PHP_EOL; + * $connection->write('hello there!' . PHP_EOL); + * … + * }); + * ``` + * + * See also the `ServerInterface` for more details. + * + * @see ServerInterface + * @see ConnectionInterface + */ +final class TcpServer extends EventEmitter implements ServerInterface +{ + private $master; + private $loop; + private $listening = false; + + /** + * Creates a plaintext TCP/IP socket server and starts listening on the given address + * + * This starts accepting new incoming connections on the given address. + * See also the `connection event` documented in the `ServerInterface` + * for more details. + * + * ```php + * $server = new TcpServer(8080, $loop); + * ``` + * + * As above, the `$uri` parameter can consist of only a port, in which case the + * server will default to listening on the localhost address `127.0.0.1`, + * which means it will not be reachable from outside of this system. + * + * In order to use a random port assignment, you can use the port `0`: + * + * ```php + * $server = new TcpServer(0, $loop); + * $address = $server->getAddress(); + * ``` + * + * In order to change the host the socket is listening on, you can provide an IP + * address through the first parameter provided to the constructor, optionally + * preceded by the `tcp://` scheme: + * + * ```php + * $server = new TcpServer('192.168.0.1:8080', $loop); + * ``` + * + * If you want to listen on an IPv6 address, you MUST enclose the host in square + * brackets: + * + * ```php + * $server = new TcpServer('[::1]:8080', $loop); + * ``` + * + * If the given URI is invalid, does not contain a port, any other scheme or if it + * contains a hostname, it will throw an `InvalidArgumentException`: + * + * ```php + * // throws InvalidArgumentException due to missing port + * $server = new TcpServer('127.0.0.1', $loop); + * ``` + * + * If the given URI appears to be valid, but listening on it fails (such as if port + * is already in use or port below 1024 may require root access etc.), it will + * throw a `RuntimeException`: + * + * ```php + * $first = new TcpServer(8080, $loop); + * + * // throws RuntimeException because port is already in use + * $second = new TcpServer(8080, $loop); + * ``` + * + * Note that these error conditions may vary depending on your system and/or + * configuration. + * See the exception message and code for more details about the actual error + * condition. + * + * Optionally, you can specify [socket context options](http://php.net/manual/en/context.socket.php) + * for the underlying stream socket resource like this: + * + * ```php + * $server = new TcpServer('[::1]:8080', $loop, array( + * 'backlog' => 200, + * 'so_reuseport' => true, + * 'ipv6_v6only' => true + * )); + * ``` + * + * Note that available [socket context options](http://php.net/manual/en/context.socket.php), + * their defaults and effects of changing these may vary depending on your system + * and/or PHP version. + * Passing unknown context options has no effect. + * + * @param string|int $uri + * @param LoopInterface $loop + * @param array $context + * @throws InvalidArgumentException if the listening address is invalid + * @throws RuntimeException if listening on this address fails (already in use etc.) + */ + public function __construct($uri, LoopInterface $loop, array $context = array()) + { + $this->loop = $loop; + + // a single port has been given => assume localhost + if ((string)(int)$uri === (string)$uri) { + $uri = '127.0.0.1:' . $uri; + } + + // assume default scheme if none has been given + if (strpos($uri, '://') === false) { + $uri = 'tcp://' . $uri; + } + + // parse_url() does not accept null ports (random port assignment) => manually remove + if (substr($uri, -2) === ':0') { + $parts = parse_url(substr($uri, 0, -2)); + if ($parts) { + $parts['port'] = 0; + } + } else { + $parts = parse_url($uri); + } + + // ensure URI contains TCP scheme, host and port + if (!$parts || !isset($parts['scheme'], $parts['host'], $parts['port']) || $parts['scheme'] !== 'tcp') { + throw new InvalidArgumentException('Invalid URI "' . $uri . '" given'); + } + + if (false === filter_var(trim($parts['host'], '[]'), FILTER_VALIDATE_IP)) { + throw new InvalidArgumentException('Given URI "' . $uri . '" does not contain a valid host IP'); + } + + $this->master = @stream_socket_server( + $uri, + $errno, + $errstr, + STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, + stream_context_create(array('socket' => $context)) + ); + if (false === $this->master) { + throw new RuntimeException('Failed to listen on "' . $uri . '": ' . $errstr, $errno); + } + stream_set_blocking($this->master, 0); + + $this->resume(); + } + + public function getAddress() + { + if (!is_resource($this->master)) { + return null; + } + + $address = stream_socket_get_name($this->master, false); + + // check if this is an IPv6 address which includes multiple colons but no square brackets + $pos = strrpos($address, ':'); + if ($pos !== false && strpos($address, ':') < $pos && substr($address, 0, 1) !== '[') { + $port = substr($address, $pos + 1); + $address = '[' . substr($address, 0, $pos) . ']:' . $port; + } + + return 'tcp://' . $address; + } + + public function pause() + { + if (!$this->listening) { + return; + } + + $this->loop->removeReadStream($this->master); + $this->listening = false; + } + + public function resume() + { + if ($this->listening || !is_resource($this->master)) { + return; + } + + $that = $this; + $this->loop->addReadStream($this->master, function ($master) use ($that) { + $newSocket = @stream_socket_accept($master); + if (false === $newSocket) { + $that->emit('error', array(new RuntimeException('Error accepting new connection'))); + + return; + } + $that->handleConnection($newSocket); + }); + $this->listening = true; + } + + public function close() + { + if (!is_resource($this->master)) { + return; + } + + $this->pause(); + fclose($this->master); + $this->removeAllListeners(); + } + + /** @internal */ + public function handleConnection($socket) + { + $this->emit('connection', array( + new Connection($socket, $this->loop) + )); + } +} diff --git a/assets/php/vendor/react/socket/src/TimeoutConnector.php b/assets/php/vendor/react/socket/src/TimeoutConnector.php new file mode 100644 index 0000000..d4eba2e --- /dev/null +++ b/assets/php/vendor/react/socket/src/TimeoutConnector.php @@ -0,0 +1,25 @@ +<?php + +namespace React\Socket; + +use React\EventLoop\LoopInterface; +use React\Promise\Timer; + +final class TimeoutConnector implements ConnectorInterface +{ + private $connector; + private $timeout; + private $loop; + + public function __construct(ConnectorInterface $connector, $timeout, LoopInterface $loop) + { + $this->connector = $connector; + $this->timeout = $timeout; + $this->loop = $loop; + } + + public function connect($uri) + { + return Timer\timeout($this->connector->connect($uri), $this->timeout, $this->loop); + } +} diff --git a/assets/php/vendor/react/socket/src/UnixConnector.php b/assets/php/vendor/react/socket/src/UnixConnector.php new file mode 100644 index 0000000..9b84ab0 --- /dev/null +++ b/assets/php/vendor/react/socket/src/UnixConnector.php @@ -0,0 +1,44 @@ +<?php + +namespace React\Socket; + +use React\EventLoop\LoopInterface; +use React\Promise; +use InvalidArgumentException; +use RuntimeException; + +/** + * Unix domain socket connector + * + * Unix domain sockets use atomic operations, so we can as well emulate + * async behavior. + */ +final class UnixConnector implements ConnectorInterface +{ + private $loop; + + public function __construct(LoopInterface $loop) + { + $this->loop = $loop; + } + + public function connect($path) + { + if (strpos($path, '://') === false) { + $path = 'unix://' . $path; + } elseif (substr($path, 0, 7) !== 'unix://') { + return Promise\reject(new InvalidArgumentException('Given URI "' . $path . '" is invalid')); + } + + $resource = @stream_socket_client($path, $errno, $errstr, 1.0); + + if (!$resource) { + return Promise\reject(new RuntimeException('Unable to connect to unix domain socket "' . $path . '": ' . $errstr, $errno)); + } + + $connection = new Connection($resource, $this->loop); + $connection->unix = true; + + return Promise\resolve($connection); + } +} diff --git a/assets/php/vendor/react/socket/src/UnixServer.php b/assets/php/vendor/react/socket/src/UnixServer.php new file mode 100644 index 0000000..8f1ed98 --- /dev/null +++ b/assets/php/vendor/react/socket/src/UnixServer.php @@ -0,0 +1,130 @@ +<?php + +namespace React\Socket; + +use Evenement\EventEmitter; +use React\EventLoop\LoopInterface; +use InvalidArgumentException; +use RuntimeException; + +/** + * The `UnixServer` class implements the `ServerInterface` and + * is responsible for accepting plaintext connections on unix domain sockets. + * + * ```php + * $server = new UnixServer('unix:///tmp/app.sock', $loop); + * ``` + * + * See also the `ServerInterface` for more details. + * + * @see ServerInterface + * @see ConnectionInterface + */ +final class UnixServer extends EventEmitter implements ServerInterface +{ + private $master; + private $loop; + private $listening = false; + + /** + * Creates a plaintext socket server and starts listening on the given unix socket + * + * This starts accepting new incoming connections on the given address. + * See also the `connection event` documented in the `ServerInterface` + * for more details. + * + * ```php + * $server = new UnixServer('unix:///tmp/app.sock', $loop); + * ``` + * + * @param string $path + * @param LoopInterface $loop + * @param array $context + * @throws InvalidArgumentException if the listening address is invalid + * @throws RuntimeException if listening on this address fails (already in use etc.) + */ + public function __construct($path, LoopInterface $loop, array $context = array()) + { + $this->loop = $loop; + + if (strpos($path, '://') === false) { + $path = 'unix://' . $path; + } elseif (substr($path, 0, 7) !== 'unix://') { + throw new InvalidArgumentException('Given URI "' . $path . '" is invalid'); + } + + $this->master = @stream_socket_server( + $path, + $errno, + $errstr, + STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, + stream_context_create(array('socket' => $context)) + ); + if (false === $this->master) { + throw new RuntimeException('Failed to listen on unix domain socket "' . $path . '": ' . $errstr, $errno); + } + stream_set_blocking($this->master, 0); + + $this->resume(); + } + + public function getAddress() + { + if (!is_resource($this->master)) { + return null; + } + + return 'unix://' . stream_socket_get_name($this->master, false); + } + + public function pause() + { + if (!$this->listening) { + return; + } + + $this->loop->removeReadStream($this->master); + $this->listening = false; + } + + public function resume() + { + if ($this->listening || !is_resource($this->master)) { + return; + } + + $that = $this; + $this->loop->addReadStream($this->master, function ($master) use ($that) { + $newSocket = @stream_socket_accept($master); + if (false === $newSocket) { + $that->emit('error', array(new RuntimeException('Error accepting new connection'))); + + return; + } + $that->handleConnection($newSocket); + }); + $this->listening = true; + } + + public function close() + { + if (!is_resource($this->master)) { + return; + } + + $this->pause(); + fclose($this->master); + $this->removeAllListeners(); + } + + /** @internal */ + public function handleConnection($socket) + { + $connection = new Connection($socket, $this->loop); + $connection->unix = true; + + $this->emit('connection', array( + $connection + )); + } +} diff --git a/assets/php/vendor/react/socket/tests/ConnectionTest.php b/assets/php/vendor/react/socket/tests/ConnectionTest.php new file mode 100644 index 0000000..d3563df --- /dev/null +++ b/assets/php/vendor/react/socket/tests/ConnectionTest.php @@ -0,0 +1,47 @@ +<?php + +namespace React\Tests\Socket; + +use React\Socket\Connection; + +class ConnectionTest extends TestCase +{ + public function testCloseConnectionWillCloseSocketResource() + { + if (defined('HHVM_VERSION')) { + $this->markTestSkipped('HHVM does not support socket operation on test memory stream'); + } + + $resource = fopen('php://memory', 'r+'); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $connection = new Connection($resource, $loop); + $connection->close(); + + $this->assertFalse(is_resource($resource)); + } + + public function testCloseConnectionWillRemoveResourceFromLoopBeforeClosingResource() + { + if (defined('HHVM_VERSION')) { + $this->markTestSkipped('HHVM does not support socket operation on test memory stream'); + } + + $resource = fopen('php://memory', 'r+'); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('addWriteStream')->with($resource); + + $onRemove = null; + $loop->expects($this->once())->method('removeWriteStream')->with($this->callback(function ($param) use (&$onRemove) { + $onRemove = is_resource($param); + return true; + })); + + $connection = new Connection($resource, $loop); + $connection->write('test'); + $connection->close(); + + $this->assertTrue($onRemove); + $this->assertFalse(is_resource($resource)); + } +} diff --git a/assets/php/vendor/react/socket/tests/ConnectorTest.php b/assets/php/vendor/react/socket/tests/ConnectorTest.php new file mode 100644 index 0000000..c8eb19b --- /dev/null +++ b/assets/php/vendor/react/socket/tests/ConnectorTest.php @@ -0,0 +1,128 @@ +<?php + +namespace React\Tests\Socket; + +use React\Socket\Connector; +use React\Promise\Promise; + +class ConnectorTest extends TestCase +{ + public function testConnectorUsesTcpAsDefaultScheme() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $promise = new Promise(function () { }); + $tcp = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $tcp->expects($this->once())->method('connect')->with('127.0.0.1:80')->willReturn($promise); + + $connector = new Connector($loop, array( + 'tcp' => $tcp + )); + + $connector->connect('127.0.0.1:80'); + } + + public function testConnectorPassedThroughHostnameIfDnsIsDisabled() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $promise = new Promise(function () { }); + $tcp = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $tcp->expects($this->once())->method('connect')->with('tcp://google.com:80')->willReturn($promise); + + $connector = new Connector($loop, array( + 'tcp' => $tcp, + 'dns' => false + )); + + $connector->connect('tcp://google.com:80'); + } + + public function testConnectorWithUnknownSchemeAlwaysFails() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $connector = new Connector($loop); + + $promise = $connector->connect('unknown://google.com:80'); + $promise->then(null, $this->expectCallableOnce()); + } + + public function testConnectorWithDisabledTcpDefaultSchemeAlwaysFails() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $connector = new Connector($loop, array( + 'tcp' => false + )); + + $promise = $connector->connect('google.com:80'); + $promise->then(null, $this->expectCallableOnce()); + } + + public function testConnectorWithDisabledTcpSchemeAlwaysFails() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $connector = new Connector($loop, array( + 'tcp' => false + )); + + $promise = $connector->connect('tcp://google.com:80'); + $promise->then(null, $this->expectCallableOnce()); + } + + public function testConnectorWithDisabledTlsSchemeAlwaysFails() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $connector = new Connector($loop, array( + 'tls' => false + )); + + $promise = $connector->connect('tls://google.com:443'); + $promise->then(null, $this->expectCallableOnce()); + } + + public function testConnectorWithDisabledUnixSchemeAlwaysFails() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $connector = new Connector($loop, array( + 'unix' => false + )); + + $promise = $connector->connect('unix://demo.sock'); + $promise->then(null, $this->expectCallableOnce()); + } + + public function testConnectorUsesGivenResolverInstance() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $promise = new Promise(function () { }); + $resolver = $this->getMockBuilder('React\Dns\Resolver\Resolver')->disableOriginalConstructor()->getMock(); + $resolver->expects($this->once())->method('resolve')->with('google.com')->willReturn($promise); + + $connector = new Connector($loop, array( + 'dns' => $resolver + )); + + $connector->connect('google.com:80'); + } + + public function testConnectorUsesResolvedHostnameIfDnsIsUsed() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $promise = new Promise(function ($resolve) { $resolve('127.0.0.1'); }); + $resolver = $this->getMockBuilder('React\Dns\Resolver\Resolver')->disableOriginalConstructor()->getMock(); + $resolver->expects($this->once())->method('resolve')->with('google.com')->willReturn($promise); + + $promise = new Promise(function () { }); + $tcp = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $tcp->expects($this->once())->method('connect')->with('tcp://127.0.0.1:80?hostname=google.com')->willReturn($promise); + + $connector = new Connector($loop, array( + 'tcp' => $tcp, + 'dns' => $resolver + )); + + $connector->connect('tcp://google.com:80'); + } +} diff --git a/assets/php/vendor/react/socket/tests/DnsConnectorTest.php b/assets/php/vendor/react/socket/tests/DnsConnectorTest.php new file mode 100644 index 0000000..3c94c39 --- /dev/null +++ b/assets/php/vendor/react/socket/tests/DnsConnectorTest.php @@ -0,0 +1,111 @@ +<?php + +namespace React\Tests\Socket; + +use React\Socket\DnsConnector; +use React\Promise; + +class DnsConnectorTest extends TestCase +{ + private $tcp; + private $resolver; + private $connector; + + public function setUp() + { + $this->tcp = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $this->resolver = $this->getMockBuilder('React\Dns\Resolver\Resolver')->disableOriginalConstructor()->getMock(); + + $this->connector = new DnsConnector($this->tcp, $this->resolver); + } + + public function testPassByResolverIfGivenIp() + { + $this->resolver->expects($this->never())->method('resolve'); + $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('127.0.0.1:80'))->will($this->returnValue(Promise\reject())); + + $this->connector->connect('127.0.0.1:80'); + } + + public function testPassThroughResolverIfGivenHost() + { + $this->resolver->expects($this->once())->method('resolve')->with($this->equalTo('google.com'))->will($this->returnValue(Promise\resolve('1.2.3.4'))); + $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('1.2.3.4:80?hostname=google.com'))->will($this->returnValue(Promise\reject())); + + $this->connector->connect('google.com:80'); + } + + public function testPassThroughResolverIfGivenHostWhichResolvesToIpv6() + { + $this->resolver->expects($this->once())->method('resolve')->with($this->equalTo('google.com'))->will($this->returnValue(Promise\resolve('::1'))); + $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('[::1]:80?hostname=google.com'))->will($this->returnValue(Promise\reject())); + + $this->connector->connect('google.com:80'); + } + + public function testPassByResolverIfGivenCompleteUri() + { + $this->resolver->expects($this->never())->method('resolve'); + $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('scheme://127.0.0.1:80/path?query#fragment'))->will($this->returnValue(Promise\reject())); + + $this->connector->connect('scheme://127.0.0.1:80/path?query#fragment'); + } + + public function testPassThroughResolverIfGivenCompleteUri() + { + $this->resolver->expects($this->once())->method('resolve')->with($this->equalTo('google.com'))->will($this->returnValue(Promise\resolve('1.2.3.4'))); + $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('scheme://1.2.3.4:80/path?query&hostname=google.com#fragment'))->will($this->returnValue(Promise\reject())); + + $this->connector->connect('scheme://google.com:80/path?query#fragment'); + } + + public function testPassThroughResolverIfGivenExplicitHost() + { + $this->resolver->expects($this->once())->method('resolve')->with($this->equalTo('google.com'))->will($this->returnValue(Promise\resolve('1.2.3.4'))); + $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('scheme://1.2.3.4:80/?hostname=google.de'))->will($this->returnValue(Promise\reject())); + + $this->connector->connect('scheme://google.com:80/?hostname=google.de'); + } + + public function testRejectsImmediatelyIfUriIsInvalid() + { + $this->resolver->expects($this->never())->method('resolve'); + $this->tcp->expects($this->never())->method('connect'); + + $promise = $this->connector->connect('////'); + + $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); + } + + public function testSkipConnectionIfDnsFails() + { + $this->resolver->expects($this->once())->method('resolve')->with($this->equalTo('example.invalid'))->will($this->returnValue(Promise\reject())); + $this->tcp->expects($this->never())->method('connect'); + + $this->connector->connect('example.invalid:80'); + } + + public function testCancelDuringDnsCancelsDnsAndDoesNotStartTcpConnection() + { + $pending = new Promise\Promise(function () { }, $this->expectCallableOnce()); + $this->resolver->expects($this->once())->method('resolve')->with($this->equalTo('example.com'))->will($this->returnValue($pending)); + $this->tcp->expects($this->never())->method('connect'); + + $promise = $this->connector->connect('example.com:80'); + $promise->cancel(); + + $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); + } + + public function testCancelDuringTcpConnectionCancelsTcpConnection() + { + $pending = new Promise\Promise(function () { }, function () { throw new \Exception(); }); + $this->resolver->expects($this->once())->method('resolve')->with($this->equalTo('example.com'))->will($this->returnValue(Promise\resolve('1.2.3.4'))); + $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('1.2.3.4:80?hostname=example.com'))->will($this->returnValue($pending)); + + $promise = $this->connector->connect('example.com:80'); + $promise->cancel(); + + $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); + } +} diff --git a/assets/php/vendor/react/socket/tests/FixedUriConnectorTest.php b/assets/php/vendor/react/socket/tests/FixedUriConnectorTest.php new file mode 100644 index 0000000..f42d74f --- /dev/null +++ b/assets/php/vendor/react/socket/tests/FixedUriConnectorTest.php @@ -0,0 +1,19 @@ +<?php + +namespace React\Tests\Socket; + +use React\Socket\FixedUriConnector; +use React\Tests\Socket\TestCase; + +class FixedUriConnectorTest extends TestCase +{ + public function testWillInvokeGivenConnector() + { + $base = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $base->expects($this->once())->method('connect')->with('test')->willReturn('ret'); + + $connector = new FixedUriConnector('test', $base); + + $this->assertEquals('ret', $connector->connect('ignored')); + } +} diff --git a/assets/php/vendor/react/socket/tests/FunctionalConnectorTest.php b/assets/php/vendor/react/socket/tests/FunctionalConnectorTest.php new file mode 100644 index 0000000..6611352 --- /dev/null +++ b/assets/php/vendor/react/socket/tests/FunctionalConnectorTest.php @@ -0,0 +1,32 @@ +<?php + +namespace React\Tests\Socket; + +use Clue\React\Block; +use React\EventLoop\Factory; +use React\Socket\Connector; +use React\Socket\TcpServer; + +class FunctionalConnectorTest extends TestCase +{ + const TIMEOUT = 1.0; + + /** @test */ + public function connectionToTcpServerShouldSucceedWithLocalhost() + { + $loop = Factory::create(); + + $server = new TcpServer(9998, $loop); + $server->on('connection', $this->expectCallableOnce()); + $server->on('connection', array($server, 'close')); + + $connector = new Connector($loop); + + $connection = Block\await($connector->connect('localhost:9998'), $loop, self::TIMEOUT); + + $this->assertInstanceOf('React\Socket\ConnectionInterface', $connection); + + $connection->close(); + $server->close(); + } +} diff --git a/assets/php/vendor/react/socket/tests/FunctionalSecureServerTest.php b/assets/php/vendor/react/socket/tests/FunctionalSecureServerTest.php new file mode 100644 index 0000000..78a59d0 --- /dev/null +++ b/assets/php/vendor/react/socket/tests/FunctionalSecureServerTest.php @@ -0,0 +1,438 @@ +<?php + +namespace React\Tests\Socket; + +use React\EventLoop\Factory; +use React\Socket\SecureServer; +use React\Socket\ConnectionInterface; +use React\Socket\TcpServer; +use React\Socket\TcpConnector; +use React\Socket\SecureConnector; +use Clue\React\Block; + +class FunctionalSecureServerTest extends TestCase +{ + const TIMEOUT = 0.5; + + public function setUp() + { + if (!function_exists('stream_socket_enable_crypto')) { + $this->markTestSkipped('Not supported on your platform (outdated HHVM?)'); + } + } + + public function testEmitsConnectionForNewConnection() + { + $loop = Factory::create(); + + $server = new TcpServer(0, $loop); + $server = new SecureServer($server, $loop, array( + 'local_cert' => __DIR__ . '/../examples/localhost.pem' + )); + $server->on('connection', $this->expectCallableOnce()); + + $connector = new SecureConnector(new TcpConnector($loop), $loop, array( + 'verify_peer' => false + )); + $promise = $connector->connect($server->getAddress()); + + Block\await($promise, $loop, self::TIMEOUT); + } + + public function testWritesDataToConnection() + { + $loop = Factory::create(); + + $server = new TcpServer(0, $loop); + $server = new SecureServer($server, $loop, array( + 'local_cert' => __DIR__ . '/../examples/localhost.pem' + )); + $server->on('connection', $this->expectCallableOnce()); + + $server->on('connection', function (ConnectionInterface $conn) { + $conn->write('foo'); + }); + + $connector = new SecureConnector(new TcpConnector($loop), $loop, array( + 'verify_peer' => false + )); + $promise = $connector->connect($server->getAddress()); + + $local = Block\await($promise, $loop, self::TIMEOUT); + /* @var $local ConnectionInterface */ + + $local->on('data', $this->expectCallableOnceWith('foo')); + + Block\sleep(self::TIMEOUT, $loop); + } + + public function testWritesDataInMultipleChunksToConnection() + { + $loop = Factory::create(); + + $server = new TcpServer(0, $loop); + $server = new SecureServer($server, $loop, array( + 'local_cert' => __DIR__ . '/../examples/localhost.pem' + )); + $server->on('connection', $this->expectCallableOnce()); + + $server->on('connection', function (ConnectionInterface $conn) { + $conn->write(str_repeat('*', 400000)); + }); + + $connector = new SecureConnector(new TcpConnector($loop), $loop, array( + 'verify_peer' => false + )); + $promise = $connector->connect($server->getAddress()); + + $local = Block\await($promise, $loop, self::TIMEOUT); + /* @var $local React\Stream\Stream */ + + $received = 0; + $local->on('data', function ($chunk) use (&$received) { + $received += strlen($chunk); + }); + + Block\sleep(self::TIMEOUT, $loop); + + $this->assertEquals(400000, $received); + } + + public function testWritesMoreDataInMultipleChunksToConnection() + { + $loop = Factory::create(); + + $server = new TcpServer(0, $loop); + $server = new SecureServer($server, $loop, array( + 'local_cert' => __DIR__ . '/../examples/localhost.pem' + )); + $server->on('connection', $this->expectCallableOnce()); + + $server->on('connection', function (ConnectionInterface $conn) { + $conn->write(str_repeat('*', 2000000)); + }); + + $connector = new SecureConnector(new TcpConnector($loop), $loop, array( + 'verify_peer' => false + )); + $promise = $connector->connect($server->getAddress()); + + $local = Block\await($promise, $loop, self::TIMEOUT); + /* @var $local React\Stream\Stream */ + + $received = 0; + $local->on('data', function ($chunk) use (&$received) { + $received += strlen($chunk); + }); + + Block\sleep(self::TIMEOUT, $loop); + + $this->assertEquals(2000000, $received); + } + + public function testEmitsDataFromConnection() + { + $loop = Factory::create(); + + $server = new TcpServer(0, $loop); + $server = new SecureServer($server, $loop, array( + 'local_cert' => __DIR__ . '/../examples/localhost.pem' + )); + $server->on('connection', $this->expectCallableOnce()); + + $once = $this->expectCallableOnceWith('foo'); + $server->on('connection', function (ConnectionInterface $conn) use ($once) { + $conn->on('data', $once); + }); + + $connector = new SecureConnector(new TcpConnector($loop), $loop, array( + 'verify_peer' => false + )); + $promise = $connector->connect($server->getAddress()); + + $local = Block\await($promise, $loop, self::TIMEOUT); + /* @var $local React\Stream\Stream */ + + $local->write("foo"); + + Block\sleep(self::TIMEOUT, $loop); + } + + public function testEmitsDataInMultipleChunksFromConnection() + { + $loop = Factory::create(); + + $server = new TcpServer(0, $loop); + $server = new SecureServer($server, $loop, array( + 'local_cert' => __DIR__ . '/../examples/localhost.pem' + )); + $server->on('connection', $this->expectCallableOnce()); + + $received = 0; + $server->on('connection', function (ConnectionInterface $conn) use (&$received) { + $conn->on('data', function ($chunk) use (&$received) { + $received += strlen($chunk); + }); + }); + + $connector = new SecureConnector(new TcpConnector($loop), $loop, array( + 'verify_peer' => false + )); + $promise = $connector->connect($server->getAddress()); + + $local = Block\await($promise, $loop, self::TIMEOUT); + /* @var $local React\Stream\Stream */ + + $local->write(str_repeat('*', 400000)); + + Block\sleep(self::TIMEOUT, $loop); + + $this->assertEquals(400000, $received); + } + + public function testPipesDataBackInMultipleChunksFromConnection() + { + $loop = Factory::create(); + + $server = new TcpServer(0, $loop); + $server = new SecureServer($server, $loop, array( + 'local_cert' => __DIR__ . '/../examples/localhost.pem' + )); + $server->on('connection', $this->expectCallableOnce()); + + $server->on('connection', function (ConnectionInterface $conn) use (&$received) { + $conn->pipe($conn); + }); + + $connector = new SecureConnector(new TcpConnector($loop), $loop, array( + 'verify_peer' => false + )); + $promise = $connector->connect($server->getAddress()); + + $local = Block\await($promise, $loop, self::TIMEOUT); + /* @var $local React\Stream\Stream */ + + $received = 0; + $local->on('data', function ($chunk) use (&$received) { + $received += strlen($chunk); + }); + + $local->write(str_repeat('*', 400000)); + + Block\sleep(self::TIMEOUT, $loop); + + $this->assertEquals(400000, $received); + } + + /** + * @requires PHP 5.6 + */ + public function testEmitsConnectionForNewTlsv11Connection() + { + $loop = Factory::create(); + + $server = new TcpServer(0, $loop); + $server = new SecureServer($server, $loop, array( + 'local_cert' => __DIR__ . '/../examples/localhost.pem', + 'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_1_SERVER + )); + $server->on('connection', $this->expectCallableOnce()); + + $connector = new SecureConnector(new TcpConnector($loop), $loop, array( + 'verify_peer' => false, + 'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT + )); + $promise = $connector->connect($server->getAddress()); + + Block\await($promise, $loop, self::TIMEOUT); + } + + /** + * @requires PHP 5.6 + */ + public function testEmitsErrorForClientWithTlsVersionMismatch() + { + $loop = Factory::create(); + + $server = new TcpServer(0, $loop); + $server = new SecureServer($server, $loop, array( + 'local_cert' => __DIR__ . '/../examples/localhost.pem', + 'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_1_SERVER|STREAM_CRYPTO_METHOD_TLSv1_2_SERVER + )); + $server->on('connection', $this->expectCallableNever()); + $server->on('error', $this->expectCallableOnce()); + + $connector = new SecureConnector(new TcpConnector($loop), $loop, array( + 'verify_peer' => false, + 'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT + )); + $promise = $connector->connect($server->getAddress()); + + $this->setExpectedException('RuntimeException', 'handshake'); + Block\await($promise, $loop, self::TIMEOUT); + } + + public function testEmitsConnectionForNewConnectionWithEncryptedCertificate() + { + $loop = Factory::create(); + + $server = new TcpServer(0, $loop); + $server = new SecureServer($server, $loop, array( + 'local_cert' => __DIR__ . '/../examples/localhost_swordfish.pem', + 'passphrase' => 'swordfish' + )); + $server->on('connection', $this->expectCallableOnce()); + + $connector = new SecureConnector(new TcpConnector($loop), $loop, array( + 'verify_peer' => false + )); + $promise = $connector->connect($server->getAddress()); + + Block\await($promise, $loop, self::TIMEOUT); + } + + public function testEmitsErrorForServerWithInvalidCertificate() + { + $loop = Factory::create(); + + $server = new TcpServer(0, $loop); + $server = new SecureServer($server, $loop, array( + 'local_cert' => 'invalid.pem' + )); + $server->on('connection', $this->expectCallableNever()); + $server->on('error', $this->expectCallableOnce()); + + $connector = new SecureConnector(new TcpConnector($loop), $loop, array( + 'verify_peer' => false + )); + $promise = $connector->connect($server->getAddress()); + + $this->setExpectedException('RuntimeException', 'handshake'); + Block\await($promise, $loop, self::TIMEOUT); + } + + public function testEmitsErrorForServerWithEncryptedCertificateMissingPassphrase() + { + $loop = Factory::create(); + + $server = new TcpServer(0, $loop); + $server = new SecureServer($server, $loop, array( + 'local_cert' => __DIR__ . '/../examples/localhost_swordfish.pem' + )); + $server->on('connection', $this->expectCallableNever()); + $server->on('error', $this->expectCallableOnce()); + + $connector = new SecureConnector(new TcpConnector($loop), $loop, array( + 'verify_peer' => false + )); + $promise = $connector->connect($server->getAddress()); + + $this->setExpectedException('RuntimeException', 'handshake'); + Block\await($promise, $loop, self::TIMEOUT); + } + + public function testEmitsErrorForServerWithEncryptedCertificateWithInvalidPassphrase() + { + $loop = Factory::create(); + + $server = new TcpServer(0, $loop); + $server = new SecureServer($server, $loop, array( + 'local_cert' => __DIR__ . '/../examples/localhost_swordfish.pem', + 'passphrase' => 'nope' + )); + $server->on('connection', $this->expectCallableNever()); + $server->on('error', $this->expectCallableOnce()); + + $connector = new SecureConnector(new TcpConnector($loop), $loop, array( + 'verify_peer' => false + )); + $promise = $connector->connect($server->getAddress()); + + $this->setExpectedException('RuntimeException', 'handshake'); + Block\await($promise, $loop, self::TIMEOUT); + } + + public function testEmitsErrorForConnectionWithPeerVerification() + { + $loop = Factory::create(); + + $server = new TcpServer(0, $loop); + $server = new SecureServer($server, $loop, array( + 'local_cert' => __DIR__ . '/../examples/localhost.pem' + )); + $server->on('connection', $this->expectCallableNever()); + $server->on('error', $this->expectCallableOnce()); + + $connector = new SecureConnector(new TcpConnector($loop), $loop, array( + 'verify_peer' => true + )); + $promise = $connector->connect($server->getAddress()); + + $promise->then(null, $this->expectCallableOnce()); + Block\sleep(self::TIMEOUT, $loop); + } + + public function testEmitsErrorIfConnectionIsCancelled() + { + if (PHP_OS !== 'Linux') { + $this->markTestSkipped('Linux only (OS is ' . PHP_OS . ')'); + } + + $loop = Factory::create(); + + $server = new TcpServer(0, $loop); + $server = new SecureServer($server, $loop, array( + 'local_cert' => __DIR__ . '/../examples/localhost.pem' + )); + $server->on('connection', $this->expectCallableNever()); + $server->on('error', $this->expectCallableOnce()); + + $connector = new SecureConnector(new TcpConnector($loop), $loop, array( + 'verify_peer' => false + )); + $promise = $connector->connect($server->getAddress()); + $promise->cancel(); + + $promise->then(null, $this->expectCallableOnce()); + Block\sleep(self::TIMEOUT, $loop); + } + + public function testEmitsNothingIfConnectionIsIdle() + { + $loop = Factory::create(); + + $server = new TcpServer(0, $loop); + $server = new SecureServer($server, $loop, array( + 'local_cert' => __DIR__ . '/../examples/localhost.pem' + )); + $server->on('connection', $this->expectCallableNever()); + $server->on('error', $this->expectCallableNever()); + + $connector = new TcpConnector($loop); + $promise = $connector->connect(str_replace('tls://', '', $server->getAddress())); + + $promise->then($this->expectCallableOnce()); + Block\sleep(self::TIMEOUT, $loop); + } + + public function testEmitsErrorIfConnectionIsNotSecureHandshake() + { + $loop = Factory::create(); + + $server = new TcpServer(0, $loop); + $server = new SecureServer($server, $loop, array( + 'local_cert' => __DIR__ . '/../examples/localhost.pem' + )); + $server->on('connection', $this->expectCallableNever()); + $server->on('error', $this->expectCallableOnce()); + + $connector = new TcpConnector($loop); + $promise = $connector->connect(str_replace('tls://', '', $server->getAddress())); + + $promise->then(function (ConnectionInterface $stream) { + $stream->write("GET / HTTP/1.0\r\n\r\n"); + }); + + Block\sleep(self::TIMEOUT, $loop); + } +} diff --git a/assets/php/vendor/react/socket/tests/FunctionalTcpServerTest.php b/assets/php/vendor/react/socket/tests/FunctionalTcpServerTest.php new file mode 100644 index 0000000..ec7855e --- /dev/null +++ b/assets/php/vendor/react/socket/tests/FunctionalTcpServerTest.php @@ -0,0 +1,324 @@ +<?php + +namespace React\Tests\Socket; + +use React\EventLoop\Factory; +use React\Socket\TcpServer; +use React\Socket\ConnectionInterface; +use React\Socket\TcpConnector; +use Clue\React\Block; + +class FunctionalTcpServerTest extends TestCase +{ + public function testEmitsConnectionForNewConnection() + { + $loop = Factory::create(); + + $server = new TcpServer(0, $loop); + $server->on('connection', $this->expectCallableOnce()); + + $connector = new TcpConnector($loop); + $promise = $connector->connect($server->getAddress()); + + $promise->then($this->expectCallableOnce()); + + Block\sleep(0.1, $loop); + } + + public function testEmitsNoConnectionForNewConnectionWhenPaused() + { + $loop = Factory::create(); + + $server = new TcpServer(0, $loop); + $server->on('connection', $this->expectCallableNever()); + $server->pause(); + + $connector = new TcpConnector($loop); + $promise = $connector->connect($server->getAddress()); + + $promise->then($this->expectCallableOnce()); + + Block\sleep(0.1, $loop); + } + + public function testEmitsConnectionForNewConnectionWhenResumedAfterPause() + { + $loop = Factory::create(); + + $server = new TcpServer(0, $loop); + $server->on('connection', $this->expectCallableOnce()); + $server->pause(); + $server->resume(); + + $connector = new TcpConnector($loop); + $promise = $connector->connect($server->getAddress()); + + $promise->then($this->expectCallableOnce()); + + Block\sleep(0.1, $loop); + } + + public function testEmitsConnectionWithRemoteIp() + { + $loop = Factory::create(); + + $server = new TcpServer(0, $loop); + $peer = null; + $server->on('connection', function (ConnectionInterface $conn) use (&$peer) { + $peer = $conn->getRemoteAddress(); + }); + + $connector = new TcpConnector($loop); + $promise = $connector->connect($server->getAddress()); + + $promise->then($this->expectCallableOnce()); + + Block\sleep(0.1, $loop); + + $this->assertContains('127.0.0.1:', $peer); + } + + public function testEmitsConnectionWithLocalIp() + { + $loop = Factory::create(); + + $server = new TcpServer(0, $loop); + $local = null; + $server->on('connection', function (ConnectionInterface $conn) use (&$local) { + $local = $conn->getLocalAddress(); + }); + + $connector = new TcpConnector($loop); + $promise = $connector->connect($server->getAddress()); + + $promise->then($this->expectCallableOnce()); + + Block\sleep(0.1, $loop); + + $this->assertContains('127.0.0.1:', $local); + $this->assertEquals($server->getAddress(), $local); + } + + public function testEmitsConnectionWithLocalIpDespiteListeningOnAll() + { + $loop = Factory::create(); + + $server = new TcpServer('0.0.0.0:0', $loop); + $local = null; + $server->on('connection', function (ConnectionInterface $conn) use (&$local) { + $local = $conn->getLocalAddress(); + }); + + $connector = new TcpConnector($loop); + $promise = $connector->connect($server->getAddress()); + + $promise->then($this->expectCallableOnce()); + + Block\sleep(0.1, $loop); + + $this->assertContains('127.0.0.1:', $local); + } + + public function testEmitsConnectionWithRemoteIpAfterConnectionIsClosedByPeer() + { + $loop = Factory::create(); + + $server = new TcpServer(0, $loop); + $peer = null; + $server->on('connection', function (ConnectionInterface $conn) use (&$peer) { + $conn->on('close', function () use ($conn, &$peer) { + $peer = $conn->getRemoteAddress(); + }); + }); + + $connector = new TcpConnector($loop); + $promise = $connector->connect($server->getAddress()); + + $client = Block\await($promise, $loop, 0.1); + $client->end(); + + Block\sleep(0.1, $loop); + + $this->assertContains('127.0.0.1:', $peer); + } + + public function testEmitsConnectionWithRemoteNullAddressAfterConnectionIsClosedLocally() + { + $loop = Factory::create(); + + $server = new TcpServer(0, $loop); + $peer = null; + $server->on('connection', function (ConnectionInterface $conn) use (&$peer) { + $conn->close(); + $peer = $conn->getRemoteAddress(); + }); + + $connector = new TcpConnector($loop); + $promise = $connector->connect($server->getAddress()); + + $promise->then($this->expectCallableOnce()); + + Block\sleep(0.1, $loop); + + $this->assertNull($peer); + } + + public function testEmitsConnectionEvenIfConnectionIsCancelled() + { + if (PHP_OS !== 'Linux') { + $this->markTestSkipped('Linux only (OS is ' . PHP_OS . ')'); + } + + $loop = Factory::create(); + + $server = new TcpServer(0, $loop); + $server->on('connection', $this->expectCallableOnce()); + + $connector = new TcpConnector($loop); + $promise = $connector->connect($server->getAddress()); + $promise->cancel(); + + $promise->then(null, $this->expectCallableOnce()); + + Block\sleep(0.1, $loop); + } + + public function testEmitsConnectionForNewIpv6Connection() + { + $loop = Factory::create(); + + try { + $server = new TcpServer('[::1]:0', $loop); + } catch (\RuntimeException $e) { + $this->markTestSkipped('Unable to start IPv6 server socket (not available on your platform?)'); + } + + $server->on('connection', $this->expectCallableOnce()); + + $connector = new TcpConnector($loop); + $promise = $connector->connect($server->getAddress()); + + $promise->then($this->expectCallableOnce()); + + Block\sleep(0.1, $loop); + } + + public function testEmitsConnectionWithRemoteIpv6() + { + $loop = Factory::create(); + + try { + $server = new TcpServer('[::1]:0', $loop); + } catch (\RuntimeException $e) { + $this->markTestSkipped('Unable to start IPv6 server socket (not available on your platform?)'); + } + + $peer = null; + $server->on('connection', function (ConnectionInterface $conn) use (&$peer) { + $peer = $conn->getRemoteAddress(); + }); + + $connector = new TcpConnector($loop); + $promise = $connector->connect($server->getAddress()); + + $promise->then($this->expectCallableOnce()); + + Block\sleep(0.1, $loop); + + $this->assertContains('[::1]:', $peer); + } + + public function testEmitsConnectionWithLocalIpv6() + { + $loop = Factory::create(); + + try { + $server = new TcpServer('[::1]:0', $loop); + } catch (\RuntimeException $e) { + $this->markTestSkipped('Unable to start IPv6 server socket (not available on your platform?)'); + } + + $local = null; + $server->on('connection', function (ConnectionInterface $conn) use (&$local) { + $local = $conn->getLocalAddress(); + }); + + $connector = new TcpConnector($loop); + $promise = $connector->connect($server->getAddress()); + + $promise->then($this->expectCallableOnce()); + + Block\sleep(0.1, $loop); + + $this->assertContains('[::1]:', $local); + $this->assertEquals($server->getAddress(), $local); + } + + public function testEmitsConnectionWithInheritedContextOptions() + { + if (defined('HHVM_VERSION') && version_compare(HHVM_VERSION, '3.13', '<')) { + // https://3v4l.org/hB4Tc + $this->markTestSkipped('Not supported on legacy HHVM < 3.13'); + } + + $loop = Factory::create(); + + $server = new TcpServer(0, $loop, array( + 'backlog' => 4 + )); + + $all = null; + $server->on('connection', function (ConnectionInterface $conn) use (&$all) { + $all = stream_context_get_options($conn->stream); + }); + + $connector = new TcpConnector($loop); + $promise = $connector->connect($server->getAddress()); + + $promise->then($this->expectCallableOnce()); + + Block\sleep(0.1, $loop); + + $this->assertEquals(array('socket' => array('backlog' => 4)), $all); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testFailsToListenOnInvalidUri() + { + $loop = Factory::create(); + + new TcpServer('///', $loop); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testFailsToListenOnUriWithoutPort() + { + $loop = Factory::create(); + + new TcpServer('127.0.0.1', $loop); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testFailsToListenOnUriWithWrongScheme() + { + $loop = Factory::create(); + + new TcpServer('udp://127.0.0.1:0', $loop); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testFailsToListenOnUriWIthHostname() + { + $loop = Factory::create(); + + new TcpServer('localhost:8080', $loop); + } +} diff --git a/assets/php/vendor/react/socket/tests/IntegrationTest.php b/assets/php/vendor/react/socket/tests/IntegrationTest.php new file mode 100644 index 0000000..24dbe37 --- /dev/null +++ b/assets/php/vendor/react/socket/tests/IntegrationTest.php @@ -0,0 +1,171 @@ +<?php + +namespace React\Tests\Socket; + +use Clue\React\Block; +use React\Dns\Resolver\Factory as ResolverFactory; +use React\EventLoop\Factory; +use React\Socket\Connector; +use React\Socket\DnsConnector; +use React\Socket\SecureConnector; +use React\Socket\TcpConnector; + +/** @group internet */ +class IntegrationTest extends TestCase +{ + const TIMEOUT = 5.0; + + /** @test */ + public function gettingStuffFromGoogleShouldWork() + { + $loop = Factory::create(); + $connector = new Connector($loop); + + $conn = Block\await($connector->connect('google.com:80'), $loop); + + $this->assertContains(':80', $conn->getRemoteAddress()); + $this->assertNotEquals('google.com:80', $conn->getRemoteAddress()); + + $conn->write("GET / HTTP/1.0\r\n\r\n"); + + $response = $this->buffer($conn, $loop, self::TIMEOUT); + + $this->assertRegExp('#^HTTP/1\.0#', $response); + } + + /** @test */ + public function gettingEncryptedStuffFromGoogleShouldWork() + { + if (!function_exists('stream_socket_enable_crypto')) { + $this->markTestSkipped('Not supported on your platform (outdated HHVM?)'); + } + + $loop = Factory::create(); + $secureConnector = new Connector($loop); + + $conn = Block\await($secureConnector->connect('tls://google.com:443'), $loop); + + $conn->write("GET / HTTP/1.0\r\n\r\n"); + + $response = $this->buffer($conn, $loop, self::TIMEOUT); + + $this->assertRegExp('#^HTTP/1\.0#', $response); + } + + /** @test */ + public function gettingEncryptedStuffFromGoogleShouldWorkIfHostIsResolvedFirst() + { + if (!function_exists('stream_socket_enable_crypto')) { + $this->markTestSkipped('Not supported on your platform (outdated HHVM?)'); + } + + $loop = Factory::create(); + + $factory = new ResolverFactory(); + $dns = $factory->create('8.8.8.8', $loop); + + $connector = new DnsConnector( + new SecureConnector( + new TcpConnector($loop), + $loop + ), + $dns + ); + + $conn = Block\await($connector->connect('google.com:443'), $loop); + + $conn->write("GET / HTTP/1.0\r\n\r\n"); + + $response = $this->buffer($conn, $loop, self::TIMEOUT); + + $this->assertRegExp('#^HTTP/1\.0#', $response); + } + + /** @test */ + public function gettingPlaintextStuffFromEncryptedGoogleShouldNotWork() + { + $loop = Factory::create(); + $connector = new Connector($loop); + + $conn = Block\await($connector->connect('google.com:443'), $loop); + + $this->assertContains(':443', $conn->getRemoteAddress()); + $this->assertNotEquals('google.com:443', $conn->getRemoteAddress()); + + $conn->write("GET / HTTP/1.0\r\n\r\n"); + + $response = $this->buffer($conn, $loop, self::TIMEOUT); + + $this->assertNotRegExp('#^HTTP/1\.0#', $response); + } + + public function testConnectingFailsIfDnsUsesInvalidResolver() + { + $loop = Factory::create(); + + $factory = new ResolverFactory(); + $dns = $factory->create('demo.invalid', $loop); + + $connector = new Connector($loop, array( + 'dns' => $dns + )); + + $this->setExpectedException('RuntimeException'); + Block\await($connector->connect('google.com:80'), $loop, self::TIMEOUT); + } + + public function testConnectingFailsIfTimeoutIsTooSmall() + { + if (!function_exists('stream_socket_enable_crypto')) { + $this->markTestSkipped('Not supported on your platform (outdated HHVM?)'); + } + + $loop = Factory::create(); + + $connector = new Connector($loop, array( + 'timeout' => 0.001 + )); + + $this->setExpectedException('RuntimeException'); + Block\await($connector->connect('google.com:80'), $loop, self::TIMEOUT); + } + + public function testSelfSignedRejectsIfVerificationIsEnabled() + { + if (!function_exists('stream_socket_enable_crypto')) { + $this->markTestSkipped('Not supported on your platform (outdated HHVM?)'); + } + + $loop = Factory::create(); + + $connector = new Connector($loop, array( + 'tls' => array( + 'verify_peer' => true + ) + )); + + $this->setExpectedException('RuntimeException'); + Block\await($connector->connect('tls://self-signed.badssl.com:443'), $loop, self::TIMEOUT); + } + + public function testSelfSignedResolvesIfVerificationIsDisabled() + { + if (!function_exists('stream_socket_enable_crypto')) { + $this->markTestSkipped('Not supported on your platform (outdated HHVM?)'); + } + + $loop = Factory::create(); + + $connector = new Connector($loop, array( + 'tls' => array( + 'verify_peer' => false + ) + )); + + $conn = Block\await($connector->connect('tls://self-signed.badssl.com:443'), $loop, self::TIMEOUT); + $conn->close(); + + // if we reach this, then everything is good + $this->assertNull(null); + } +} diff --git a/assets/php/vendor/react/socket/tests/LimitingServerTest.php b/assets/php/vendor/react/socket/tests/LimitingServerTest.php new file mode 100644 index 0000000..2cc9a58 --- /dev/null +++ b/assets/php/vendor/react/socket/tests/LimitingServerTest.php @@ -0,0 +1,195 @@ +<?php + +namespace React\Tests\Socket; + +use React\Socket\LimitingServer; +use React\Socket\TcpServer; +use React\EventLoop\Factory; +use Clue\React\Block; + +class LimitingServerTest extends TestCase +{ + public function testGetAddressWillBePassedThroughToTcpServer() + { + $tcp = $this->getMockBuilder('React\Socket\ServerInterface')->getMock(); + $tcp->expects($this->once())->method('getAddress')->willReturn('127.0.0.1:1234'); + + $server = new LimitingServer($tcp, 100); + + $this->assertEquals('127.0.0.1:1234', $server->getAddress()); + } + + public function testPauseWillBePassedThroughToTcpServer() + { + $tcp = $this->getMockBuilder('React\Socket\ServerInterface')->getMock(); + $tcp->expects($this->once())->method('pause'); + + $server = new LimitingServer($tcp, 100); + + $server->pause(); + } + + public function testPauseTwiceWillBePassedThroughToTcpServerOnce() + { + $tcp = $this->getMockBuilder('React\Socket\ServerInterface')->getMock(); + $tcp->expects($this->once())->method('pause'); + + $server = new LimitingServer($tcp, 100); + + $server->pause(); + $server->pause(); + } + + public function testResumeWillBePassedThroughToTcpServer() + { + $tcp = $this->getMockBuilder('React\Socket\ServerInterface')->getMock(); + $tcp->expects($this->once())->method('resume'); + + $server = new LimitingServer($tcp, 100); + + $server->pause(); + $server->resume(); + } + + public function testResumeTwiceWillBePassedThroughToTcpServerOnce() + { + $tcp = $this->getMockBuilder('React\Socket\ServerInterface')->getMock(); + $tcp->expects($this->once())->method('resume'); + + $server = new LimitingServer($tcp, 100); + + $server->pause(); + $server->resume(); + $server->resume(); + } + + public function testCloseWillBePassedThroughToTcpServer() + { + $tcp = $this->getMockBuilder('React\Socket\ServerInterface')->getMock(); + $tcp->expects($this->once())->method('close'); + + $server = new LimitingServer($tcp, 100); + + $server->close(); + } + + public function testSocketErrorWillBeForwarded() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $tcp = new TcpServer(0, $loop); + + $server = new LimitingServer($tcp, 100); + + $server->on('error', $this->expectCallableOnce()); + + $tcp->emit('error', array(new \RuntimeException('test'))); + } + + public function testSocketConnectionWillBeForwarded() + { + $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $tcp = new TcpServer(0, $loop); + + $server = new LimitingServer($tcp, 100); + $server->on('connection', $this->expectCallableOnceWith($connection)); + $server->on('error', $this->expectCallableNever()); + + $tcp->emit('connection', array($connection)); + + $this->assertEquals(array($connection), $server->getConnections()); + } + + public function testSocketConnectionWillBeClosedOnceLimitIsReached() + { + $first = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $first->expects($this->never())->method('close'); + $second = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $second->expects($this->once())->method('close'); + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $tcp = new TcpServer(0, $loop); + + $server = new LimitingServer($tcp, 1); + $server->on('connection', $this->expectCallableOnceWith($first)); + $server->on('error', $this->expectCallableOnce()); + + $tcp->emit('connection', array($first)); + $tcp->emit('connection', array($second)); + } + + public function testPausingServerWillBePausedOnceLimitIsReached() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('addReadStream'); + $loop->expects($this->once())->method('removeReadStream'); + + $tcp = new TcpServer(0, $loop); + + $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + + $server = new LimitingServer($tcp, 1, true); + + $tcp->emit('connection', array($connection)); + } + + public function testSocketDisconnectionWillRemoveFromList() + { + $loop = Factory::create(); + + $tcp = new TcpServer(0, $loop); + + $socket = stream_socket_client($tcp->getAddress()); + fclose($socket); + + $server = new LimitingServer($tcp, 100); + $server->on('connection', $this->expectCallableOnce()); + $server->on('error', $this->expectCallableNever()); + + Block\sleep(0.1, $loop); + + $this->assertEquals(array(), $server->getConnections()); + } + + public function testPausingServerWillEmitOnlyOneButAcceptTwoConnectionsDueToOperatingSystem() + { + $loop = Factory::create(); + + $server = new TcpServer(0, $loop); + $server = new LimitingServer($server, 1, true); + $server->on('connection', $this->expectCallableOnce()); + $server->on('error', $this->expectCallableNever()); + + $first = stream_socket_client($server->getAddress()); + $second = stream_socket_client($server->getAddress()); + + Block\sleep(0.1, $loop); + + fclose($first); + fclose($second); + } + + public function testPausingServerWillEmitTwoConnectionsFromBacklog() + { + $loop = Factory::create(); + + $twice = $this->createCallableMock(); + $twice->expects($this->exactly(2))->method('__invoke'); + + $server = new TcpServer(0, $loop); + $server = new LimitingServer($server, 1, true); + $server->on('connection', $twice); + $server->on('error', $this->expectCallableNever()); + + $first = stream_socket_client($server->getAddress()); + fclose($first); + $second = stream_socket_client($server->getAddress()); + fclose($second); + + Block\sleep(0.1, $loop); + } +} diff --git a/assets/php/vendor/react/socket/tests/SecureConnectorTest.php b/assets/php/vendor/react/socket/tests/SecureConnectorTest.php new file mode 100644 index 0000000..0b3a702 --- /dev/null +++ b/assets/php/vendor/react/socket/tests/SecureConnectorTest.php @@ -0,0 +1,74 @@ +<?php + +namespace React\Tests\Socket; + +use React\Promise; +use React\Socket\SecureConnector; + +class SecureConnectorTest extends TestCase +{ + private $loop; + private $tcp; + private $connector; + + public function setUp() + { + if (!function_exists('stream_socket_enable_crypto')) { + $this->markTestSkipped('Not supported on your platform (outdated HHVM?)'); + } + + $this->loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $this->tcp = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $this->connector = new SecureConnector($this->tcp, $this->loop); + } + + public function testConnectionWillWaitForTcpConnection() + { + $pending = new Promise\Promise(function () { }); + $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('example.com:80'))->will($this->returnValue($pending)); + + $promise = $this->connector->connect('example.com:80'); + + $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); + } + + public function testConnectionWithCompleteUriWillBePassedThroughExpectForScheme() + { + $pending = new Promise\Promise(function () { }); + $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('example.com:80/path?query#fragment'))->will($this->returnValue($pending)); + + $this->connector->connect('tls://example.com:80/path?query#fragment'); + } + + public function testConnectionToInvalidSchemeWillReject() + { + $this->tcp->expects($this->never())->method('connect'); + + $promise = $this->connector->connect('tcp://example.com:80'); + + $promise->then(null, $this->expectCallableOnce()); + } + + public function testCancelDuringTcpConnectionCancelsTcpConnection() + { + $pending = new Promise\Promise(function () { }, function () { throw new \Exception(); }); + $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('example.com:80'))->will($this->returnValue($pending)); + + $promise = $this->connector->connect('example.com:80'); + $promise->cancel(); + + $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); + } + + public function testConnectionWillBeClosedAndRejectedIfConnectioIsNoStream() + { + $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection->expects($this->once())->method('close'); + + $this->tcp->expects($this->once())->method('connect')->with($this->equalTo('example.com:80'))->willReturn(Promise\resolve($connection)); + + $promise = $this->connector->connect('example.com:80'); + + $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); + } +} diff --git a/assets/php/vendor/react/socket/tests/SecureIntegrationTest.php b/assets/php/vendor/react/socket/tests/SecureIntegrationTest.php new file mode 100644 index 0000000..8c9ba14 --- /dev/null +++ b/assets/php/vendor/react/socket/tests/SecureIntegrationTest.php @@ -0,0 +1,204 @@ +<?php + +namespace React\Tests\Socket; + +use React\EventLoop\Factory as LoopFactory; +use React\Socket\TcpServer; +use React\Socket\SecureServer; +use React\Socket\TcpConnector; +use React\Socket\SecureConnector; +use Clue\React\Block; +use React\Promise\Promise; +use Evenement\EventEmitterInterface; +use React\Promise\Deferred; +use React\Socket\ConnectionInterface; + +class SecureIntegrationTest extends TestCase +{ + const TIMEOUT = 0.5; + + private $loop; + private $server; + private $connector; + private $address; + + public function setUp() + { + if (!function_exists('stream_socket_enable_crypto')) { + $this->markTestSkipped('Not supported on your platform (outdated HHVM?)'); + } + + $this->loop = LoopFactory::create(); + $this->server = new TcpServer(0, $this->loop); + $this->server = new SecureServer($this->server, $this->loop, array( + 'local_cert' => __DIR__ . '/../examples/localhost.pem' + )); + $this->address = $this->server->getAddress(); + $this->connector = new SecureConnector(new TcpConnector($this->loop), $this->loop, array('verify_peer' => false)); + } + + public function tearDown() + { + if ($this->server !== null) { + $this->server->close(); + $this->server = null; + } + } + + public function testConnectToServer() + { + $client = Block\await($this->connector->connect($this->address), $this->loop, self::TIMEOUT); + /* @var $client ConnectionInterface */ + + $client->close(); + + // if we reach this, then everything is good + $this->assertNull(null); + } + + public function testConnectToServerEmitsConnection() + { + $promiseServer = $this->createPromiseForEvent($this->server, 'connection', $this->expectCallableOnce()); + + $promiseClient = $this->connector->connect($this->address); + + list($_, $client) = Block\awaitAll(array($promiseServer, $promiseClient), $this->loop, self::TIMEOUT); + /* @var $client ConnectionInterface */ + + $client->close(); + } + + public function testSendSmallDataToServerReceivesOneChunk() + { + // server expects one connection which emits one data event + $received = new Deferred(); + $this->server->on('connection', function (ConnectionInterface $peer) use ($received) { + $peer->on('data', function ($chunk) use ($received) { + $received->resolve($chunk); + }); + }); + + $client = Block\await($this->connector->connect($this->address), $this->loop, self::TIMEOUT); + /* @var $client ConnectionInterface */ + + $client->write('hello'); + + // await server to report one "data" event + $data = Block\await($received->promise(), $this->loop, self::TIMEOUT); + + $client->close(); + + $this->assertEquals('hello', $data); + } + + public function testSendDataWithEndToServerReceivesAllData() + { + $disconnected = new Deferred(); + $this->server->on('connection', function (ConnectionInterface $peer) use ($disconnected) { + $received = ''; + $peer->on('data', function ($chunk) use (&$received) { + $received .= $chunk; + }); + $peer->on('close', function () use (&$received, $disconnected) { + $disconnected->resolve($received); + }); + }); + + $client = Block\await($this->connector->connect($this->address), $this->loop, self::TIMEOUT); + /* @var $client ConnectionInterface */ + + $data = str_repeat('a', 200000); + $client->end($data); + + // await server to report connection "close" event + $received = Block\await($disconnected->promise(), $this->loop, self::TIMEOUT); + + $this->assertEquals($data, $received); + } + + public function testSendDataWithoutEndingToServerReceivesAllData() + { + $received = ''; + $this->server->on('connection', function (ConnectionInterface $peer) use (&$received) { + $peer->on('data', function ($chunk) use (&$received) { + $received .= $chunk; + }); + }); + + $client = Block\await($this->connector->connect($this->address), $this->loop, self::TIMEOUT); + /* @var $client ConnectionInterface */ + + $data = str_repeat('d', 200000); + $client->write($data); + + // buffer incoming data for 0.1s (should be plenty of time) + Block\sleep(0.1, $this->loop); + + $client->close(); + + $this->assertEquals($data, $received); + } + + public function testConnectToServerWhichSendsSmallDataReceivesOneChunk() + { + $this->server->on('connection', function (ConnectionInterface $peer) { + $peer->write('hello'); + }); + + $client = Block\await($this->connector->connect($this->address), $this->loop, self::TIMEOUT); + /* @var $client ConnectionInterface */ + + // await client to report one "data" event + $receive = $this->createPromiseForEvent($client, 'data', $this->expectCallableOnceWith('hello')); + Block\await($receive, $this->loop, self::TIMEOUT); + + $client->close(); + } + + public function testConnectToServerWhichSendsDataWithEndReceivesAllData() + { + $data = str_repeat('b', 100000); + $this->server->on('connection', function (ConnectionInterface $peer) use ($data) { + $peer->end($data); + }); + + $client = Block\await($this->connector->connect($this->address), $this->loop, self::TIMEOUT); + /* @var $client ConnectionInterface */ + + // await data from client until it closes + $received = $this->buffer($client, $this->loop, self::TIMEOUT); + + $this->assertEquals($data, $received); + } + + public function testConnectToServerWhichSendsDataWithoutEndingReceivesAllData() + { + $data = str_repeat('c', 100000); + $this->server->on('connection', function (ConnectionInterface $peer) use ($data) { + $peer->write($data); + }); + + $client = Block\await($this->connector->connect($this->address), $this->loop, self::TIMEOUT); + /* @var $client ConnectionInterface */ + + // buffer incoming data for 0.1s (should be plenty of time) + $received = ''; + $client->on('data', function ($chunk) use (&$received) { + $received .= $chunk; + }); + Block\sleep(0.1, $this->loop); + + $client->close(); + + $this->assertEquals($data, $received); + } + + private function createPromiseForEvent(EventEmitterInterface $emitter, $event, $fn) + { + return new Promise(function ($resolve) use ($emitter, $event, $fn) { + $emitter->on($event, function () use ($resolve, $fn) { + $resolve(call_user_func_array($fn, func_get_args())); + }); + }); + } +} diff --git a/assets/php/vendor/react/socket/tests/SecureServerTest.php b/assets/php/vendor/react/socket/tests/SecureServerTest.php new file mode 100644 index 0000000..92c641f --- /dev/null +++ b/assets/php/vendor/react/socket/tests/SecureServerTest.php @@ -0,0 +1,105 @@ +<?php + +namespace React\Tests\Socket; + +use React\Socket\SecureServer; +use React\Socket\TcpServer; + +class SecureServerTest extends TestCase +{ + public function setUp() + { + if (!function_exists('stream_socket_enable_crypto')) { + $this->markTestSkipped('Not supported on your platform (outdated HHVM?)'); + } + } + + public function testGetAddressWillBePassedThroughToTcpServer() + { + $tcp = $this->getMockBuilder('React\Socket\ServerInterface')->getMock(); + $tcp->expects($this->once())->method('getAddress')->willReturn('tcp://127.0.0.1:1234'); + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $server = new SecureServer($tcp, $loop, array()); + + $this->assertEquals('tls://127.0.0.1:1234', $server->getAddress()); + } + + public function testGetAddressWillReturnNullIfTcpServerReturnsNull() + { + $tcp = $this->getMockBuilder('React\Socket\ServerInterface')->getMock(); + $tcp->expects($this->once())->method('getAddress')->willReturn(null); + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $server = new SecureServer($tcp, $loop, array()); + + $this->assertNull($server->getAddress()); + } + + public function testPauseWillBePassedThroughToTcpServer() + { + $tcp = $this->getMockBuilder('React\Socket\ServerInterface')->getMock(); + $tcp->expects($this->once())->method('pause'); + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $server = new SecureServer($tcp, $loop, array()); + + $server->pause(); + } + + public function testResumeWillBePassedThroughToTcpServer() + { + $tcp = $this->getMockBuilder('React\Socket\ServerInterface')->getMock(); + $tcp->expects($this->once())->method('resume'); + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $server = new SecureServer($tcp, $loop, array()); + + $server->resume(); + } + + public function testCloseWillBePassedThroughToTcpServer() + { + $tcp = $this->getMockBuilder('React\Socket\ServerInterface')->getMock(); + $tcp->expects($this->once())->method('close'); + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $server = new SecureServer($tcp, $loop, array()); + + $server->close(); + } + + public function testConnectionWillBeEndedWithErrorIfItIsNotAStream() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $tcp = new TcpServer(0, $loop); + + $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $connection->expects($this->once())->method('end'); + + $server = new SecureServer($tcp, $loop, array()); + + $server->on('error', $this->expectCallableOnce()); + + $tcp->emit('connection', array($connection)); + } + + public function testSocketErrorWillBeForwarded() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $tcp = new TcpServer(0, $loop); + + $server = new SecureServer($tcp, $loop, array()); + + $server->on('error', $this->expectCallableOnce()); + + $tcp->emit('error', array(new \RuntimeException('test'))); + } +} diff --git a/assets/php/vendor/react/socket/tests/ServerTest.php b/assets/php/vendor/react/socket/tests/ServerTest.php new file mode 100644 index 0000000..14fdb2c --- /dev/null +++ b/assets/php/vendor/react/socket/tests/ServerTest.php @@ -0,0 +1,173 @@ +<?php + +namespace React\Tests\Socket; + +use React\EventLoop\Factory; +use React\Socket\Server; +use React\Socket\TcpConnector; +use React\Socket\UnixConnector; +use Clue\React\Block; +use React\Socket\ConnectionInterface; + +class ServerTest extends TestCase +{ + const TIMEOUT = 0.1; + + public function testCreateServerWithZeroPortAssignsRandomPort() + { + $loop = Factory::create(); + + $server = new Server(0, $loop); + $this->assertNotEquals(0, $server->getAddress()); + $server->close(); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testConstructorThrowsForInvalidUri() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $server = new Server('invalid URI', $loop); + } + + public function testConstructorCreatesExpectedTcpServer() + { + $loop = Factory::create(); + + $server = new Server(0, $loop); + + $connector = new TcpConnector($loop); + $connector->connect($server->getAddress()) + ->then($this->expectCallableOnce(), $this->expectCallableNever()); + + $connection = Block\await($connector->connect($server->getAddress()), $loop, self::TIMEOUT); + + $connection->close(); + $server->close(); + } + + public function testConstructorCreatesExpectedUnixServer() + { + $loop = Factory::create(); + + $server = new Server($this->getRandomSocketUri(), $loop); + + $connector = new UnixConnector($loop); + $connector->connect($server->getAddress()) + ->then($this->expectCallableOnce(), $this->expectCallableNever()); + + $connection = Block\await($connector->connect($server->getAddress()), $loop, self::TIMEOUT); + + $connection->close(); + $server->close(); + } + + public function testEmitsConnectionForNewConnection() + { + $loop = Factory::create(); + + $server = new Server(0, $loop); + $server->on('connection', $this->expectCallableOnce()); + + $client = stream_socket_client($server->getAddress()); + + Block\sleep(0.1, $loop); + } + + public function testDoesNotEmitConnectionForNewConnectionToPausedServer() + { + $loop = Factory::create(); + + $server = new Server(0, $loop); + $server->pause(); + $server->on('connection', $this->expectCallableNever()); + + $client = stream_socket_client($server->getAddress()); + + Block\sleep(0.1, $loop); + } + + public function testDoesEmitConnectionForNewConnectionToResumedServer() + { + $loop = Factory::create(); + + $server = new Server(0, $loop); + $server->pause(); + $server->on('connection', $this->expectCallableOnce()); + + $client = stream_socket_client($server->getAddress()); + + Block\sleep(0.1, $loop); + + $server->resume(); + Block\sleep(0.1, $loop); + } + + public function testDoesNotAllowConnectionToClosedServer() + { + $loop = Factory::create(); + + $server = new Server(0, $loop); + $server->on('connection', $this->expectCallableNever()); + $address = $server->getAddress(); + $server->close(); + + $client = @stream_socket_client($address); + + Block\sleep(0.1, $loop); + + $this->assertFalse($client); + } + + public function testEmitsConnectionWithInheritedContextOptions() + { + if (defined('HHVM_VERSION') && version_compare(HHVM_VERSION, '3.13', '<')) { + // https://3v4l.org/hB4Tc + $this->markTestSkipped('Not supported on legacy HHVM < 3.13'); + } + + $loop = Factory::create(); + + $server = new Server(0, $loop, array( + 'backlog' => 4 + )); + + $all = null; + $server->on('connection', function (ConnectionInterface $conn) use (&$all) { + $all = stream_context_get_options($conn->stream); + }); + + $client = stream_socket_client($server->getAddress()); + + Block\sleep(0.1, $loop); + + $this->assertEquals(array('socket' => array('backlog' => 4)), $all); + } + + public function testDoesNotEmitSecureConnectionForNewPlainConnection() + { + if (!function_exists('stream_socket_enable_crypto')) { + $this->markTestSkipped('Not supported on your platform (outdated HHVM?)'); + } + + $loop = Factory::create(); + + $server = new Server('tls://127.0.0.1:0', $loop, array( + 'tls' => array( + 'local_cert' => __DIR__ . '/../examples/localhost.pem' + ) + )); + $server->on('connection', $this->expectCallableNever()); + + $client = stream_socket_client(str_replace('tls://', '', $server->getAddress())); + + Block\sleep(0.1, $loop); + } + + private function getRandomSocketUri() + { + return "unix://" . sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid(rand(), true) . '.sock'; + } +} diff --git a/assets/php/vendor/react/socket/tests/Stub/CallableStub.php b/assets/php/vendor/react/socket/tests/Stub/CallableStub.php new file mode 100644 index 0000000..1b197eb --- /dev/null +++ b/assets/php/vendor/react/socket/tests/Stub/CallableStub.php @@ -0,0 +1,10 @@ +<?php + +namespace React\Tests\Socket\Stub; + +class CallableStub +{ + public function __invoke() + { + } +} diff --git a/assets/php/vendor/react/socket/tests/Stub/ConnectionStub.php b/assets/php/vendor/react/socket/tests/Stub/ConnectionStub.php new file mode 100644 index 0000000..844b2ad --- /dev/null +++ b/assets/php/vendor/react/socket/tests/Stub/ConnectionStub.php @@ -0,0 +1,63 @@ +<?php + +namespace React\Tests\Socket\Stub; + +use Evenement\EventEmitter; +use React\Socket\ConnectionInterface; +use React\Stream\WritableStreamInterface; +use React\Stream\Util; + +class ConnectionStub extends EventEmitter implements ConnectionInterface +{ + private $data = ''; + + public function isReadable() + { + return true; + } + + public function isWritable() + { + return true; + } + + public function pause() + { + } + + public function resume() + { + } + + public function pipe(WritableStreamInterface $dest, array $options = array()) + { + Util::pipe($this, $dest, $options); + + return $dest; + } + + public function write($data) + { + $this->data .= $data; + + return true; + } + + public function end($data = null) + { + } + + public function close() + { + } + + public function getData() + { + return $this->data; + } + + public function getRemoteAddress() + { + return '127.0.0.1'; + } +} diff --git a/assets/php/vendor/react/socket/tests/Stub/ServerStub.php b/assets/php/vendor/react/socket/tests/Stub/ServerStub.php new file mode 100644 index 0000000..d9e74f4 --- /dev/null +++ b/assets/php/vendor/react/socket/tests/Stub/ServerStub.php @@ -0,0 +1,18 @@ +<?php + +namespace React\Tests\Socket\Stub; + +use Evenement\EventEmitter; +use React\Socket\ServerInterface; + +class ServerStub extends EventEmitter implements ServerInterface +{ + public function getAddress() + { + return '127.0.0.1:80'; + } + + public function close() + { + } +} diff --git a/assets/php/vendor/react/socket/tests/TcpConnectorTest.php b/assets/php/vendor/react/socket/tests/TcpConnectorTest.php new file mode 100644 index 0000000..e3575a7 --- /dev/null +++ b/assets/php/vendor/react/socket/tests/TcpConnectorTest.php @@ -0,0 +1,255 @@ +<?php + +namespace React\Tests\Socket; + +use Clue\React\Block; +use React\EventLoop\Factory; +use React\Socket\ConnectionInterface; +use React\Socket\TcpConnector; +use React\Socket\TcpServer; + +class TcpConnectorTest extends TestCase +{ + const TIMEOUT = 0.1; + + /** @test */ + public function connectionToEmptyPortShouldFail() + { + $loop = Factory::create(); + + $connector = new TcpConnector($loop); + $connector->connect('127.0.0.1:9999') + ->then($this->expectCallableNever(), $this->expectCallableOnce()); + + $loop->run(); + } + + /** @test */ + public function connectionToTcpServerShouldAddResourceToLoop() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $connector = new TcpConnector($loop); + + $server = new TcpServer(0, $loop); + + $valid = false; + $loop->expects($this->once())->method('addWriteStream')->with($this->callback(function ($arg) use (&$valid) { + $valid = is_resource($arg); + return true; + })); + $connector->connect($server->getAddress()); + + $this->assertTrue($valid); + } + + /** @test */ + public function connectionToTcpServerShouldSucceed() + { + $loop = Factory::create(); + + $server = new TcpServer(9999, $loop); + $server->on('connection', $this->expectCallableOnce()); + $server->on('connection', array($server, 'close')); + + $connector = new TcpConnector($loop); + + $connection = Block\await($connector->connect('127.0.0.1:9999'), $loop, self::TIMEOUT); + + $this->assertInstanceOf('React\Socket\ConnectionInterface', $connection); + + $connection->close(); + } + + /** @test */ + public function connectionToTcpServerShouldSucceedWithRemoteAdressSameAsTarget() + { + $loop = Factory::create(); + + $server = new TcpServer(9999, $loop); + $server->on('connection', array($server, 'close')); + + $connector = new TcpConnector($loop); + + $connection = Block\await($connector->connect('127.0.0.1:9999'), $loop, self::TIMEOUT); + /* @var $connection ConnectionInterface */ + + $this->assertEquals('tcp://127.0.0.1:9999', $connection->getRemoteAddress()); + + $connection->close(); + } + + /** @test */ + public function connectionToTcpServerShouldSucceedWithLocalAdressOnLocalhost() + { + $loop = Factory::create(); + + $server = new TcpServer(9999, $loop); + $server->on('connection', array($server, 'close')); + + $connector = new TcpConnector($loop); + + $connection = Block\await($connector->connect('127.0.0.1:9999'), $loop, self::TIMEOUT); + /* @var $connection ConnectionInterface */ + + $this->assertContains('tcp://127.0.0.1:', $connection->getLocalAddress()); + $this->assertNotEquals('tcp://127.0.0.1:9999', $connection->getLocalAddress()); + + $connection->close(); + } + + /** @test */ + public function connectionToTcpServerShouldSucceedWithNullAddressesAfterConnectionClosed() + { + $loop = Factory::create(); + + $server = new TcpServer(9999, $loop); + $server->on('connection', array($server, 'close')); + + $connector = new TcpConnector($loop); + + $connection = Block\await($connector->connect('127.0.0.1:9999'), $loop, self::TIMEOUT); + /* @var $connection ConnectionInterface */ + + $connection->close(); + + $this->assertNull($connection->getRemoteAddress()); + $this->assertNull($connection->getLocalAddress()); + } + + /** @test */ + public function connectionToTcpServerWillCloseWhenOtherSideCloses() + { + $loop = Factory::create(); + + // immediately close connection and server once connection is in + $server = new TcpServer(0, $loop); + $server->on('connection', function (ConnectionInterface $conn) use ($server) { + $conn->close(); + $server->close(); + }); + + $once = $this->expectCallableOnce(); + $connector = new TcpConnector($loop); + $connector->connect($server->getAddress())->then(function (ConnectionInterface $conn) use ($once) { + $conn->write('hello'); + $conn->on('close', $once); + }); + + $loop->run(); + } + + /** @test */ + public function connectionToEmptyIp6PortShouldFail() + { + $loop = Factory::create(); + + $connector = new TcpConnector($loop); + $connector + ->connect('[::1]:9999') + ->then($this->expectCallableNever(), $this->expectCallableOnce()); + + $loop->run(); + } + + /** @test */ + public function connectionToIp6TcpServerShouldSucceed() + { + $loop = Factory::create(); + + try { + $server = new TcpServer('[::1]:9999', $loop); + } catch (\Exception $e) { + $this->markTestSkipped('Unable to start IPv6 server socket (IPv6 not supported on this system?)'); + } + + $server->on('connection', $this->expectCallableOnce()); + $server->on('connection', array($server, 'close')); + + $connector = new TcpConnector($loop); + + $connection = Block\await($connector->connect('[::1]:9999'), $loop, self::TIMEOUT); + /* @var $connection ConnectionInterface */ + + $this->assertEquals('tcp://[::1]:9999', $connection->getRemoteAddress()); + + $this->assertContains('tcp://[::1]:', $connection->getLocalAddress()); + $this->assertNotEquals('tcp://[::1]:9999', $connection->getLocalAddress()); + + $connection->close(); + } + + /** @test */ + public function connectionToHostnameShouldFailImmediately() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $connector = new TcpConnector($loop); + $connector->connect('www.google.com:80')->then( + $this->expectCallableNever(), + $this->expectCallableOnce() + ); + } + + /** @test */ + public function connectionToInvalidPortShouldFailImmediately() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $connector = new TcpConnector($loop); + $connector->connect('255.255.255.255:12345678')->then( + $this->expectCallableNever(), + $this->expectCallableOnce() + ); + } + + /** @test */ + public function connectionToInvalidSchemeShouldFailImmediately() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $connector = new TcpConnector($loop); + $connector->connect('tls://google.com:443')->then( + $this->expectCallableNever(), + $this->expectCallableOnce() + ); + } + + /** @test */ + public function cancellingConnectionShouldRemoveResourceFromLoopAndCloseResource() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $connector = new TcpConnector($loop); + + $server = new TcpServer(0, $loop); + + $loop->expects($this->once())->method('addWriteStream'); + $promise = $connector->connect($server->getAddress()); + + $resource = null; + $valid = false; + $loop->expects($this->once())->method('removeWriteStream')->with($this->callback(function ($arg) use (&$resource, &$valid) { + $resource = $arg; + $valid = is_resource($arg); + return true; + })); + $promise->cancel(); + + $this->assertTrue($valid); + $this->assertFalse(is_resource($resource)); + } + + /** @test */ + public function cancellingConnectionShouldRejectPromise() + { + $loop = Factory::create(); + $connector = new TcpConnector($loop); + + $server = new TcpServer(0, $loop); + + $promise = $connector->connect($server->getAddress()); + $promise->cancel(); + + $this->setExpectedException('RuntimeException', 'Cancelled'); + Block\await($promise, $loop); + } +} diff --git a/assets/php/vendor/react/socket/tests/TcpServerTest.php b/assets/php/vendor/react/socket/tests/TcpServerTest.php new file mode 100644 index 0000000..72b3c28 --- /dev/null +++ b/assets/php/vendor/react/socket/tests/TcpServerTest.php @@ -0,0 +1,285 @@ +<?php + +namespace React\Tests\Socket; + +use Clue\React\Block; +use React\EventLoop\Factory; +use React\Socket\TcpServer; +use React\Stream\DuplexResourceStream; + +class TcpServerTest extends TestCase +{ + private $loop; + private $server; + private $port; + + private function createLoop() + { + return Factory::create(); + } + + /** + * @covers React\Socket\TcpServer::__construct + * @covers React\Socket\TcpServer::getAddress + */ + public function setUp() + { + $this->loop = $this->createLoop(); + $this->server = new TcpServer(0, $this->loop); + + $this->port = parse_url($this->server->getAddress(), PHP_URL_PORT); + } + + /** + * @covers React\Socket\TcpServer::handleConnection + */ + public function testConnection() + { + $client = stream_socket_client('tcp://localhost:'.$this->port); + + $this->server->on('connection', $this->expectCallableOnce()); + + $this->tick(); + } + + /** + * @covers React\Socket\TcpServer::handleConnection + */ + public function testConnectionWithManyClients() + { + $client1 = stream_socket_client('tcp://localhost:'.$this->port); + $client2 = stream_socket_client('tcp://localhost:'.$this->port); + $client3 = stream_socket_client('tcp://localhost:'.$this->port); + + $this->server->on('connection', $this->expectCallableExactly(3)); + $this->tick(); + $this->tick(); + $this->tick(); + } + + public function testDataEventWillNotBeEmittedWhenClientSendsNoData() + { + $client = stream_socket_client('tcp://localhost:'.$this->port); + + $mock = $this->expectCallableNever(); + + $this->server->on('connection', function ($conn) use ($mock) { + $conn->on('data', $mock); + }); + $this->tick(); + $this->tick(); + } + + public function testDataWillBeEmittedWithDataClientSends() + { + $client = stream_socket_client('tcp://localhost:'.$this->port); + + fwrite($client, "foo\n"); + + $mock = $this->expectCallableOnceWith("foo\n"); + + $this->server->on('connection', function ($conn) use ($mock) { + $conn->on('data', $mock); + }); + $this->tick(); + $this->tick(); + } + + public function testDataWillBeEmittedEvenWhenClientShutsDownAfterSending() + { + $client = stream_socket_client('tcp://localhost:' . $this->port); + fwrite($client, "foo\n"); + stream_socket_shutdown($client, STREAM_SHUT_WR); + + $mock = $this->expectCallableOnceWith("foo\n"); + + $this->server->on('connection', function ($conn) use ($mock) { + $conn->on('data', $mock); + }); + $this->tick(); + $this->tick(); + } + + public function testLoopWillEndWhenServerIsClosed() + { + // explicitly unset server because we already call close() + $this->server->close(); + $this->server = null; + + $this->loop->run(); + + // if we reach this, then everything is good + $this->assertNull(null); + } + + public function testCloseTwiceIsNoOp() + { + $this->server->close(); + $this->server->close(); + + // if we reach this, then everything is good + $this->assertNull(null); + } + + public function testGetAddressAfterCloseReturnsNull() + { + $this->server->close(); + $this->assertNull($this->server->getAddress()); + } + + public function testLoopWillEndWhenServerIsClosedAfterSingleConnection() + { + $client = stream_socket_client('tcp://localhost:' . $this->port); + + // explicitly unset server because we only accept a single connection + // and then already call close() + $server = $this->server; + $this->server = null; + + $server->on('connection', function ($conn) use ($server) { + $conn->close(); + $server->close(); + }); + + $this->loop->run(); + + // if we reach this, then everything is good + $this->assertNull(null); + } + + public function testDataWillBeEmittedInMultipleChunksWhenClientSendsExcessiveAmounts() + { + $client = stream_socket_client('tcp://localhost:' . $this->port); + $stream = new DuplexResourceStream($client, $this->loop); + + $bytes = 1024 * 1024; + $stream->end(str_repeat('*', $bytes)); + + $mock = $this->expectCallableOnce(); + + // explicitly unset server because we only accept a single connection + // and then already call close() + $server = $this->server; + $this->server = null; + + $received = 0; + $server->on('connection', function ($conn) use ($mock, &$received, $server) { + // count number of bytes received + $conn->on('data', function ($data) use (&$received) { + $received += strlen($data); + }); + + $conn->on('end', $mock); + + // do not await any further connections in order to let the loop terminate + $server->close(); + }); + + $this->loop->run(); + + $this->assertEquals($bytes, $received); + } + + public function testConnectionDoesNotEndWhenClientDoesNotClose() + { + $client = stream_socket_client('tcp://localhost:'.$this->port); + + $mock = $this->expectCallableNever(); + + $this->server->on('connection', function ($conn) use ($mock) { + $conn->on('end', $mock); + }); + $this->tick(); + $this->tick(); + } + + /** + * @covers React\Socket\Connection::end + */ + public function testConnectionDoesEndWhenClientCloses() + { + $client = stream_socket_client('tcp://localhost:'.$this->port); + + fclose($client); + + $mock = $this->expectCallableOnce(); + + $this->server->on('connection', function ($conn) use ($mock) { + $conn->on('end', $mock); + }); + $this->tick(); + $this->tick(); + } + + public function testCtorAddsResourceToLoop() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('addReadStream'); + + $server = new TcpServer(0, $loop); + } + + public function testResumeWithoutPauseIsNoOp() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('addReadStream'); + + $server = new TcpServer(0, $loop); + $server->resume(); + } + + public function testPauseRemovesResourceFromLoop() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('removeReadStream'); + + $server = new TcpServer(0, $loop); + $server->pause(); + } + + public function testPauseAfterPauseIsNoOp() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('removeReadStream'); + + $server = new TcpServer(0, $loop); + $server->pause(); + $server->pause(); + } + + public function testCloseRemovesResourceFromLoop() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('removeReadStream'); + + $server = new TcpServer(0, $loop); + $server->close(); + } + + /** + * @expectedException RuntimeException + */ + public function testListenOnBusyPortThrows() + { + if (DIRECTORY_SEPARATOR === '\\') { + $this->markTestSkipped('Windows supports listening on same port multiple times'); + } + + $another = new TcpServer($this->port, $this->loop); + } + + /** + * @covers React\Socket\TcpServer::close + */ + public function tearDown() + { + if ($this->server) { + $this->server->close(); + } + } + + private function tick() + { + Block\sleep(0, $this->loop); + } +} diff --git a/assets/php/vendor/react/socket/tests/TestCase.php b/assets/php/vendor/react/socket/tests/TestCase.php new file mode 100644 index 0000000..e87fc2f --- /dev/null +++ b/assets/php/vendor/react/socket/tests/TestCase.php @@ -0,0 +1,101 @@ +<?php + +namespace React\Tests\Socket; + +use React\Stream\ReadableStreamInterface; +use React\EventLoop\LoopInterface; +use Clue\React\Block; +use React\Promise\Promise; +use PHPUnit\Framework\TestCase as BaseTestCase; + +class TestCase extends BaseTestCase +{ + protected function expectCallableExactly($amount) + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->exactly($amount)) + ->method('__invoke'); + + return $mock; + } + + protected function expectCallableOnce() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke'); + + return $mock; + } + + protected function expectCallableOnceWith($value) + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($value); + + return $mock; + } + + protected function expectCallableNever() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->never()) + ->method('__invoke'); + + return $mock; + } + + protected function createCallableMock() + { + return $this->getMockBuilder('React\Tests\Socket\Stub\CallableStub')->getMock(); + } + + protected function buffer(ReadableStreamInterface $stream, LoopInterface $loop, $timeout) + { + if (!$stream->isReadable()) { + return ''; + } + + return Block\await(new Promise( + function ($resolve, $reject) use ($stream) { + $buffer = ''; + $stream->on('data', function ($chunk) use (&$buffer) { + $buffer .= $chunk; + }); + + $stream->on('error', $reject); + + $stream->on('close', function () use (&$buffer, $resolve) { + $resolve($buffer); + }); + }, + function () use ($stream) { + $stream->close(); + throw new \RuntimeException(); + } + ), $loop, $timeout); + } + + public function setExpectedException($exception, $exceptionMessage = '', $exceptionCode = null) + { + if (method_exists($this, 'expectException')) { + // PHPUnit 5+ + $this->expectException($exception); + if ($exceptionMessage !== '') { + $this->expectExceptionMessage($exceptionMessage); + } + if ($exceptionCode !== null) { + $this->expectExceptionCode($exceptionCode); + } + } else { + // legacy PHPUnit 4 + parent::setExpectedException($exception, $exceptionMessage, $exceptionCode); + } + } +} diff --git a/assets/php/vendor/react/socket/tests/TimeoutConnectorTest.php b/assets/php/vendor/react/socket/tests/TimeoutConnectorTest.php new file mode 100644 index 0000000..64787d9 --- /dev/null +++ b/assets/php/vendor/react/socket/tests/TimeoutConnectorTest.php @@ -0,0 +1,103 @@ +<?php + +namespace React\Tests\Socket; + +use React\Socket\TimeoutConnector; +use React\Promise; +use React\EventLoop\Factory; + +class TimeoutConnectorTest extends TestCase +{ + public function testRejectsOnTimeout() + { + $promise = new Promise\Promise(function () { }); + + $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector->expects($this->once())->method('connect')->with('google.com:80')->will($this->returnValue($promise)); + + $loop = Factory::create(); + + $timeout = new TimeoutConnector($connector, 0.01, $loop); + + $timeout->connect('google.com:80')->then( + $this->expectCallableNever(), + $this->expectCallableOnce() + ); + + $loop->run(); + } + + public function testRejectsWhenConnectorRejects() + { + $promise = Promise\reject(new \RuntimeException()); + + $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector->expects($this->once())->method('connect')->with('google.com:80')->will($this->returnValue($promise)); + + $loop = Factory::create(); + + $timeout = new TimeoutConnector($connector, 5.0, $loop); + + $timeout->connect('google.com:80')->then( + $this->expectCallableNever(), + $this->expectCallableOnce() + ); + + $loop->run(); + } + + public function testResolvesWhenConnectorResolves() + { + $promise = Promise\resolve(); + + $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector->expects($this->once())->method('connect')->with('google.com:80')->will($this->returnValue($promise)); + + $loop = Factory::create(); + + $timeout = new TimeoutConnector($connector, 5.0, $loop); + + $timeout->connect('google.com:80')->then( + $this->expectCallableOnce(), + $this->expectCallableNever() + ); + + $loop->run(); + } + + public function testRejectsAndCancelsPendingPromiseOnTimeout() + { + $promise = new Promise\Promise(function () { }, $this->expectCallableOnce()); + + $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector->expects($this->once())->method('connect')->with('google.com:80')->will($this->returnValue($promise)); + + $loop = Factory::create(); + + $timeout = new TimeoutConnector($connector, 0.01, $loop); + + $timeout->connect('google.com:80')->then( + $this->expectCallableNever(), + $this->expectCallableOnce() + ); + + $loop->run(); + } + + public function testCancelsPendingPromiseOnCancel() + { + $promise = new Promise\Promise(function () { }, function () { throw new \Exception(); }); + + $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $connector->expects($this->once())->method('connect')->with('google.com:80')->will($this->returnValue($promise)); + + $loop = Factory::create(); + + $timeout = new TimeoutConnector($connector, 0.01, $loop); + + $out = $timeout->connect('google.com:80'); + $out->cancel(); + + $out->then($this->expectCallableNever(), $this->expectCallableOnce()); + } +} diff --git a/assets/php/vendor/react/socket/tests/UnixConnectorTest.php b/assets/php/vendor/react/socket/tests/UnixConnectorTest.php new file mode 100644 index 0000000..1564064 --- /dev/null +++ b/assets/php/vendor/react/socket/tests/UnixConnectorTest.php @@ -0,0 +1,64 @@ +<?php + +namespace React\Tests\Socket; + +use React\Socket\ConnectionInterface; +use React\Socket\UnixConnector; + +class UnixConnectorTest extends TestCase +{ + private $loop; + private $connector; + + public function setUp() + { + $this->loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $this->connector = new UnixConnector($this->loop); + } + + public function testInvalid() + { + $promise = $this->connector->connect('google.com:80'); + $promise->then(null, $this->expectCallableOnce()); + } + + public function testInvalidScheme() + { + $promise = $this->connector->connect('tcp://google.com:80'); + $promise->then(null, $this->expectCallableOnce()); + } + + public function testValid() + { + // random unix domain socket path + $path = sys_get_temp_dir() . '/test' . uniqid() . '.sock'; + + // temporarily create unix domain socket server to connect to + $server = stream_socket_server('unix://' . $path, $errno, $errstr); + + // skip test if we can not create a test server (Windows etc.) + if (!$server) { + $this->markTestSkipped('Unable to create socket "' . $path . '": ' . $errstr . '(' . $errno .')'); + return; + } + + // tests succeeds if we get notified of successful connection + $promise = $this->connector->connect($path); + $promise->then($this->expectCallableOnce()); + + // remember remote and local address of this connection and close again + $remote = $local = false; + $promise->then(function(ConnectionInterface $conn) use (&$remote, &$local) { + $remote = $conn->getRemoteAddress(); + $local = $conn->getLocalAddress(); + $conn->close(); + }); + + // clean up server + fclose($server); + unlink($path); + + $this->assertNull($local); + $this->assertEquals('unix://' . $path, $remote); + } +} diff --git a/assets/php/vendor/react/socket/tests/UnixServerTest.php b/assets/php/vendor/react/socket/tests/UnixServerTest.php new file mode 100644 index 0000000..10f7e4f --- /dev/null +++ b/assets/php/vendor/react/socket/tests/UnixServerTest.php @@ -0,0 +1,283 @@ +<?php + +namespace React\Tests\Socket; + +use Clue\React\Block; +use React\EventLoop\Factory; +use React\Socket\UnixServer; +use React\Stream\DuplexResourceStream; + +class UnixServerTest extends TestCase +{ + private $loop; + private $server; + private $uds; + + /** + * @covers React\Socket\UnixServer::__construct + * @covers React\Socket\UnixServer::getAddress + */ + public function setUp() + { + $this->loop = Factory::create(); + $this->uds = $this->getRandomSocketUri(); + $this->server = new UnixServer($this->uds, $this->loop); + } + + /** + * @covers React\Socket\UnixServer::handleConnection + */ + public function testConnection() + { + $client = stream_socket_client($this->uds); + + $this->server->on('connection', $this->expectCallableOnce()); + $this->tick(); + } + + /** + * @covers React\Socket\UnixServer::handleConnection + */ + public function testConnectionWithManyClients() + { + $client1 = stream_socket_client($this->uds); + $client2 = stream_socket_client($this->uds); + $client3 = stream_socket_client($this->uds); + + $this->server->on('connection', $this->expectCallableExactly(3)); + $this->tick(); + $this->tick(); + $this->tick(); + } + + public function testDataEventWillNotBeEmittedWhenClientSendsNoData() + { + $client = stream_socket_client($this->uds); + + $mock = $this->expectCallableNever(); + + $this->server->on('connection', function ($conn) use ($mock) { + $conn->on('data', $mock); + }); + $this->tick(); + $this->tick(); + } + + public function testDataWillBeEmittedWithDataClientSends() + { + $client = stream_socket_client($this->uds); + + fwrite($client, "foo\n"); + + $mock = $this->expectCallableOnceWith("foo\n"); + + $this->server->on('connection', function ($conn) use ($mock) { + $conn->on('data', $mock); + }); + $this->tick(); + $this->tick(); + } + + public function testDataWillBeEmittedEvenWhenClientShutsDownAfterSending() + { + $client = stream_socket_client($this->uds); + fwrite($client, "foo\n"); + stream_socket_shutdown($client, STREAM_SHUT_WR); + + $mock = $this->expectCallableOnceWith("foo\n"); + + $this->server->on('connection', function ($conn) use ($mock) { + $conn->on('data', $mock); + }); + $this->tick(); + $this->tick(); + } + + public function testLoopWillEndWhenServerIsClosed() + { + // explicitly unset server because we already call close() + $this->server->close(); + $this->server = null; + + $this->loop->run(); + + // if we reach this, then everything is good + $this->assertNull(null); + } + + public function testCloseTwiceIsNoOp() + { + $this->server->close(); + $this->server->close(); + + // if we reach this, then everything is good + $this->assertNull(null); + } + + public function testGetAddressAfterCloseReturnsNull() + { + $this->server->close(); + $this->assertNull($this->server->getAddress()); + } + + public function testLoopWillEndWhenServerIsClosedAfterSingleConnection() + { + $client = stream_socket_client($this->uds); + + // explicitly unset server because we only accept a single connection + // and then already call close() + $server = $this->server; + $this->server = null; + + $server->on('connection', function ($conn) use ($server) { + $conn->close(); + $server->close(); + }); + + $this->loop->run(); + + // if we reach this, then everything is good + $this->assertNull(null); + } + + public function testDataWillBeEmittedInMultipleChunksWhenClientSendsExcessiveAmounts() + { + $client = stream_socket_client($this->uds); + $stream = new DuplexResourceStream($client, $this->loop); + + $bytes = 1024 * 1024; + $stream->end(str_repeat('*', $bytes)); + + $mock = $this->expectCallableOnce(); + + // explicitly unset server because we only accept a single connection + // and then already call close() + $server = $this->server; + $this->server = null; + + $received = 0; + $server->on('connection', function ($conn) use ($mock, &$received, $server) { + // count number of bytes received + $conn->on('data', function ($data) use (&$received) { + $received += strlen($data); + }); + + $conn->on('end', $mock); + + // do not await any further connections in order to let the loop terminate + $server->close(); + }); + + $this->loop->run(); + + $this->assertEquals($bytes, $received); + } + + public function testConnectionDoesNotEndWhenClientDoesNotClose() + { + $client = stream_socket_client($this->uds); + + $mock = $this->expectCallableNever(); + + $this->server->on('connection', function ($conn) use ($mock) { + $conn->on('end', $mock); + }); + $this->tick(); + $this->tick(); + } + + /** + * @covers React\Socket\Connection::end + */ + public function testConnectionDoesEndWhenClientCloses() + { + $client = stream_socket_client($this->uds); + + fclose($client); + + $mock = $this->expectCallableOnce(); + + $this->server->on('connection', function ($conn) use ($mock) { + $conn->on('end', $mock); + }); + $this->tick(); + $this->tick(); + } + + public function testCtorAddsResourceToLoop() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('addReadStream'); + + $server = new UnixServer($this->getRandomSocketUri(), $loop); + } + + public function testResumeWithoutPauseIsNoOp() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('addReadStream'); + + $server = new UnixServer($this->getRandomSocketUri(), $loop); + $server->resume(); + } + + public function testPauseRemovesResourceFromLoop() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('removeReadStream'); + + $server = new UnixServer($this->getRandomSocketUri(), $loop); + $server->pause(); + } + + public function testPauseAfterPauseIsNoOp() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('removeReadStream'); + + $server = new UnixServer($this->getRandomSocketUri(), $loop); + $server->pause(); + $server->pause(); + } + + public function testCloseRemovesResourceFromLoop() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('removeReadStream'); + + $server = new UnixServer($this->getRandomSocketUri(), $loop); + $server->close(); + } + + /** + * @expectedException RuntimeException + */ + public function testListenOnBusyPortThrows() + { + if (DIRECTORY_SEPARATOR === '\\') { + $this->markTestSkipped('Windows supports listening on same port multiple times'); + } + + $another = new UnixServer($this->uds, $this->loop); + } + + /** + * @covers React\Socket\UnixServer::close + */ + public function tearDown() + { + if ($this->server) { + $this->server->close(); + } + } + + private function getRandomSocketUri() + { + return "unix://" . sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid(rand(), true) . '.sock'; + } + + private function tick() + { + Block\sleep(0, $this->loop); + } +} diff --git a/assets/php/vendor/react/stream/.gitignore b/assets/php/vendor/react/stream/.gitignore new file mode 100644 index 0000000..987e2a2 --- /dev/null +++ b/assets/php/vendor/react/stream/.gitignore @@ -0,0 +1,2 @@ +composer.lock +vendor diff --git a/assets/php/vendor/react/stream/.travis.yml b/assets/php/vendor/react/stream/.travis.yml new file mode 100644 index 0000000..f4e3376 --- /dev/null +++ b/assets/php/vendor/react/stream/.travis.yml @@ -0,0 +1,50 @@ +language: php + +php: +# - 5.3 # requires old distro, see below + - 5.4 + - 5.5 + - 5.6 + - 7.0 +# - 7.0 # Mac OS X test setup, ignore errors, see below + - 7.1 + - 7.2 + - nightly # ignore errors, see below + - hhvm # ignore errors, see below + +# lock distro so new future defaults will not break the build +dist: trusty + +matrix: + include: + - php: 5.3 + dist: precise + include: + - os: osx + language: generic + php: 7.0 # just to look right on travis + env: + - PACKAGE: php70 + allow_failures: + - php: nightly + - php: hhvm + - os: osx + +install: + # OSX install inspired by https://github.com/kiler129/TravisCI-OSX-PHP + - | + if [[ "${TRAVIS_OS_NAME}" == "osx" ]]; then + brew tap homebrew/homebrew-php + echo "Installing PHP ..." + brew install "${PACKAGE}" + brew install "${PACKAGE}"-xdebug + brew link "${PACKAGE}" + echo "Installing composer ..." + curl -s http://getcomposer.org/installer | php + mv composer.phar /usr/local/bin/composer + fi + - composer install --no-interaction + +script: + - vendor/bin/phpunit --coverage-text + - time php examples/91-benchmark-throughput.php diff --git a/assets/php/vendor/react/stream/CHANGELOG.md b/assets/php/vendor/react/stream/CHANGELOG.md new file mode 100644 index 0000000..f64815d --- /dev/null +++ b/assets/php/vendor/react/stream/CHANGELOG.md @@ -0,0 +1,377 @@ +# Changelog + +## 0.7.7 (2018-01-19) + +* Improve test suite by fixing forward compatibility with upcoming EventLoop + releases, avoid risky tests and add test group to skip integration tests + relying on internet connection and apply appropriate test timeouts. + (#128, #131 and #132 by @clue) + +## 0.7.6 (2017-12-21) + +* Fix: Work around reading from unbuffered pipe stream in legacy PHP < 5.4.28 and PHP < 5.5.12 + (#126 by @clue) + +* Improve test suite by simplifying test bootstrapping logic via Composer and + test against PHP 7.2 + (#127 by @clue and #124 by @carusogabriel) + +## 0.7.5 (2017-11-20) + +* Fix: Igore excessive `fopen()` mode flags for `WritableResourceStream` + (#119 by @clue) + +* Fix: Fix forward compatibility with upcoming EventLoop releases + (#121 by @clue) + +* Restructure examples to ease getting started + (#123 by @clue) + +* Improve test suite by adding forward compatibility with PHPUnit 6 and + ignore Mac OS X test failures for now until Travis tests work again + (#122 by @gabriel-caruso and #120 by @clue) + +## 0.7.4 (2017-10-11) + +* Fix: Remove event listeners from `CompositeStream` once closed and + remove undocumented left-over `close` event argument + (#116 by @clue) + +* Minor documentation improvements: Fix wrong class name in example, + fix typos in README and + fix forward compatibility with upcoming EventLoop releases in example + (#113 by @docteurklein and #114 and #115 by @clue) + +* Improve test suite by running against Mac OS X on Travis + (#112 by @clue) + +## 0.7.3 (2017-08-05) + +* Improvement: Support Événement 3.0 a long side 2.0 and 1.0 + (#108 by @WyriHaximus) + +* Readme: Corrected loop initialization in usage example + (#109 by @pulyavin) + +* Travis: Lock linux distribution preventing future builds from breaking + (#110 by @clue) + +## 0.7.2 (2017-06-15) + +* Bug fix: WritableResourceStream: Close the underlying stream when closing the stream. + (#107 by @WyriHaximus) + +## 0.7.1 (2017-05-20) + +* Feature: Add optional `$writeChunkSize` parameter to limit maximum number of + bytes to write at once. + (#105 by @clue) + + ```php + $stream = new WritableResourceStream(STDOUT, $loop, null, 8192); + ``` + +* Ignore HHVM test failures for now until Travis tests work again + (#106 by @clue) + +## 0.7.0 (2017-05-04) + +* Removed / BC break: Remove deprecated and unneeded functionality + (#45, #87, #90, #91 and #93 by @clue) + + * Remove deprecated `Stream` class, use `DuplexResourceStream` instead + (#87 by @clue) + + * Remove public `$buffer` property, use new constructor parameters instead + (#91 by @clue) + + * Remove public `$stream` property from all resource streams + (#90 by @clue) + + * Remove undocumented and now unused `ReadableStream` and `WritableStream` + (#93 by @clue) + + * Remove `BufferedSink` + (#45 by @clue) + +* Feature / BC break: Simplify `ThroughStream` by using data callback instead of + inheritance. It is now a direct implementation of `DuplexStreamInterface`. + (#88 and #89 by @clue) + + ```php + $through = new ThroughStream(function ($data) { + return json_encode($data) . PHP_EOL; + }); + $through->on('data', $this->expectCallableOnceWith("[2, true]\n")); + + $through->write(array(2, true)); + ``` + +* Feature / BC break: The `CompositeStream` starts closed if either side is + already closed and forwards pause to pipe source on first write attempt. + (#96 and #103 by @clue) + + If either side of the composite stream closes, it will also close the other + side. We now also ensure that if either side is already closed during + instantiation, it will also close the other side. + +* BC break: Mark all classes as `final` and + mark internal API as `private` to discourage inheritance + (#95 and #99 by @clue) + +* Feature / BC break: Only emit `error` event for fatal errors + (#92 by @clue) + + > The `error` event was previously also allowed to be emitted for non-fatal + errors, but our implementations actually only ever emitted this as a fatal + error and then closed the stream. + +* Feature: Explicitly allow custom events and exclude any semantics + (#97 by @clue) + +* Support legacy PHP 5.3 through PHP 7.1 and HHVM and improve usage documentation + (#100 and #102 by @clue) + +* Actually require all dependencies so this is self-contained and improve + forward compatibility with EventLoop v1.0 and v0.5 + (#94 and #98 by @clue) + +## 0.6.0 (2017-03-26) + +* Feature / Fix / BC break: Add `DuplexResourceStream` and deprecate `Stream` + (#85 by @clue) + + ```php + // old (does still work for BC reasons) + $stream = new Stream($connection, $loop); + + // new + $stream = new DuplexResourceStream($connection, $loop); + ``` + + Note that the `DuplexResourceStream` now rejects read-only or write-only + streams, so this may affect BC. If you want a read-only or write-only + resource, use `ReadableResourceStream` or `WritableResourceStream` instead of + `DuplexResourceStream`. + + > BC note: This class was previously called `Stream`. The `Stream` class still + exists for BC reasons and will be removed in future versions of this package. + +* Feature / BC break: Add `WritableResourceStream` (previously called `Buffer`) + (#84 by @clue) + + ```php + // old + $stream = new Buffer(STDOUT, $loop); + + // new + $stream = new WritableResourceStream(STDOUT, $loop); + ``` + +* Feature: Add `ReadableResourceStream` + (#83 by @clue) + + ```php + $stream = new ReadableResourceStream(STDIN, $loop); + ``` + +* Fix / BC Break: Enforce using non-blocking I/O + (#46 by @clue) + + > BC note: This is known to affect process pipes on Windows which do not + support non-blocking I/O and could thus block the whole EventLoop previously. + +* Feature / Fix / BC break: Consistent semantics for + `DuplexStreamInterface::end()` to ensure it SHOULD also end readable side + (#86 by @clue) + +* Fix: Do not use unbuffered reads on pipe streams for legacy PHP < 5.4 + (#80 by @clue) + +## 0.5.0 (2017-03-08) + +* Feature / BC break: Consistent `end` event semantics (EOF) + (#70 by @clue) + + The `end` event will now only be emitted for a *successful* end, not if the + stream closes due to an unrecoverable `error` event or if you call `close()` + explicitly. + If you want to detect when the stream closes (terminates), use the `close` + event instead. + +* BC break: Remove custom (undocumented) `full-drain` event from `Buffer` + (#63 and #68 by @clue) + + > The `full-drain` event was undocumented and mostly used internally. + Relying on this event has attracted some low-quality code in the past, so + we've removed this from the public API in order to work out a better + solution instead. + If you want to detect when the buffer finishes flushing data to the stream, + you may want to look into its `end()` method or the `close` event instead. + +* Feature / BC break: Consistent event semantics and documentation, + explicitly state *when* events will be emitted and *which* arguments they + receive. + (#73 and #69 by @clue) + + The documentation now explicitly defines each event and its arguments. + Custom events and event arguments are still supported. + Most notably, all defined events only receive inherently required event + arguments and no longer transmit the instance they are emitted on for + consistency and performance reasons. + + ```php + // old (inconsistent and not supported by all implementations) + $stream->on('data', function ($data, $stream) { + // process $data + }); + + // new (consistent throughout the whole ecosystem) + $stream->on('data', function ($data) use ($stream) { + // process $data + }); + ``` + + > This mostly adds documentation (and thus some stricter, consistent + definitions) for the existing behavior, it does NOT define any major + changes otherwise. + Most existing code should be compatible with these changes, unless + it relied on some undocumented/unintended semantics. + +* Feature / BC break: Consistent method semantics and documentation + (#72 by @clue) + + > This mostly adds documentation (and thus some stricter, consistent + definitions) for the existing behavior, it does NOT define any major + changes otherwise. + Most existing code should be compatible with these changes, unless + it relied on some undocumented/unintended semantics. + +* Feature: Consistent `pipe()` semantics for closed and closing streams + (#71 from @clue) + + The source stream will now always be paused via `pause()` when the + destination stream closes. Also, properly stop piping if the source + stream closes and remove all event forwarding. + +* Improve test suite by adding PHPUnit to `require-dev` and improving coverage. + (#74 and #75 by @clue, #66 by @nawarian) + +## 0.4.6 (2017-01-25) + +* Feature: The `Buffer` can now be injected into the `Stream` (or be used standalone) + (#62 by @clue) + +* Fix: Forward `close` event only once for `CompositeStream` and `ThroughStream` + (#60 by @clue) + +* Fix: Consistent `close` event behavior for `Buffer` + (#61 by @clue) + +## 0.4.5 (2016-11-13) + +* Feature: Support setting read buffer size to `null` (infinite) + (#42 by @clue) + +* Fix: Do not emit `full-drain` event if `Buffer` is closed during `drain` event + (#55 by @clue) + +* Vastly improved performance by factor of 10x to 20x. + Raise default buffer sizes to 64 KiB and simplify and improve error handling + and unneeded function calls. + (#53, #55, #56 by @clue) + +## 0.4.4 (2016-08-22) + +* Bug fix: Emit `error` event and close `Stream` when accessing the underlying + stream resource fails with a permanent error. + (#52 and #40 by @clue, #25 by @lysenkobv) + +* Bug fix: Do not emit empty `data` event if nothing has been read (stream reached EOF) + (#39 by @clue) + +* Bug fix: Ignore empty writes to `Buffer` + (#51 by @clue) + +* Add benchmarking script to measure throughput in CI + (#41 by @clue) + +## 0.4.3 (2015-10-07) + +* Bug fix: Read buffer to 0 fixes error with libevent and large quantity of I/O (@mbonneau) +* Bug fix: No double-write during drain call (@arnaud-lb) +* Bug fix: Support HHVM (@clue) +* Adjust compatibility to 5.3 (@clue) + +## 0.4.2 (2014-09-09) + +* Added DuplexStreamInterface +* Stream sets stream resources to non-blocking +* Fixed potential race condition in pipe + +## 0.4.1 (2014-04-13) + +* Bug fix: v0.3.4 changes merged for v0.4.1 + +## 0.3.4 (2014-03-30) + +* Bug fix: [Stream] Fixed 100% CPU spike from non-empty write buffer on closed stream + +## 0.4.0 (2014-02-02) + +* BC break: Bump minimum PHP version to PHP 5.4, remove 5.3 specific hacks +* BC break: Update to Evenement 2.0 +* Dependency: Autoloading and filesystem structure now PSR-4 instead of PSR-0 + +## 0.3.3 (2013-07-08) + +* Bug fix: [Stream] Correctly detect closed connections + +## 0.3.2 (2013-05-10) + +* Bug fix: [Stream] Make sure CompositeStream is closed properly + +## 0.3.1 (2013-04-21) + +* Bug fix: [Stream] Allow any `ReadableStreamInterface` on `BufferedSink::createPromise()` + +## 0.3.0 (2013-04-14) + +* Feature: [Stream] Factory method for BufferedSink + +## 0.2.6 (2012-12-26) + +* Version bump + +## 0.2.5 (2012-11-26) + +* Feature: Make BufferedSink trigger progress events on the promise (@jsor) + +## 0.2.4 (2012-11-18) + +* Feature: Added ThroughStream, CompositeStream, ReadableStream and WritableStream +* Feature: Added BufferedSink + +## 0.2.3 (2012-11-14) + +* Version bump + +## 0.2.2 (2012-10-28) + +* Version bump + +## 0.2.1 (2012-10-14) + +* Bug fix: Check for EOF in `Buffer::write()` + +## 0.2.0 (2012-09-10) + +* Version bump + +## 0.1.1 (2012-07-12) + +* Bug fix: Testing and functional against PHP >= 5.3.3 and <= 5.3.8 + +## 0.1.0 (2012-07-11) + +* First tagged release diff --git a/assets/php/vendor/react/stream/LICENSE b/assets/php/vendor/react/stream/LICENSE new file mode 100644 index 0000000..a808108 --- /dev/null +++ b/assets/php/vendor/react/stream/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2012 Igor Wiedler, Chris Boden + +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/assets/php/vendor/react/stream/README.md b/assets/php/vendor/react/stream/README.md new file mode 100644 index 0000000..c362534 --- /dev/null +++ b/assets/php/vendor/react/stream/README.md @@ -0,0 +1,1224 @@ +# Stream + +[](https://travis-ci.org/reactphp/stream) + +Event-driven readable and writable streams for non-blocking I/O in [ReactPHP](https://reactphp.org/). + +In order to make the [EventLoop](https://github.com/reactphp/event-loop) +easier to use, this component introduces the powerful concept of "streams". +Streams allow you to efficiently process huge amounts of data (such as a multi +Gigabyte file download) in small chunks without having to store everything in +memory at once. +They are very similar to the streams found in PHP itself, +but have an interface more suited for async, non-blocking I/O. + +**Table of contents** + +* [Stream usage](#stream-usage) + * [ReadableStreamInterface](#readablestreaminterface) + * [data event](#data-event) + * [end event](#end-event) + * [error event](#error-event) + * [close event](#close-event) + * [isReadable()](#isreadable) + * [pause()](#pause) + * [resume()](#resume) + * [pipe()](#pipe) + * [close()](#close) + * [WritableStreamInterface](#writablestreaminterface) + * [drain event](#drain-event) + * [pipe event](#pipe-event) + * [error event](#error-event-1) + * [close event](#close-event-1) + * [isWritable()](#iswritable) + * [write()](#write) + * [end()](#end) + * [close()](#close-1) + * [DuplexStreamInterface](#duplexstreaminterface) +* [Creating streams](#creating-streams) + * [ReadableResourceStream](#readableresourcestream) + * [WritableResourceStream](#writableresourcestream) + * [DuplexResourceStream](#duplexresourcestream) + * [ThroughStream](#throughstream) + * [CompositeStream](#compositestream) +* [Usage](#usage) +* [Install](#install) +* [Tests](#tests) +* [License](#license) +* [More](#more) + +## Stream usage + +ReactPHP uses the concept of "streams" throughout its ecosystem to provide a +consistent higher-level abstraction for processing streams of arbitrary data +contents and size. +While a stream itself is a quite low-level concept, it can be used as a powerful +abstraction to build higher-level components and protocols on top. + +If you're new to this concept, it helps to think of them as a water pipe: +You can consume water from a source or you can produce water and forward (pipe) +it to any destination (sink). + +Similarly, streams can either be + +* readable (such as `STDIN` terminal input) or +* writable (such as `STDOUT` terminal output) or +* duplex (both readable *and* writable, such as a TCP/IP connection) + +Accordingly, this package defines the following three interfaces + +* [`ReadableStreamInterface`](#readablestreaminterface) +* [`WritableStreamInterface`](#writablestreaminterface) +* [`DuplexStreamInterface`](#duplexstreaminterface) + +### ReadableStreamInterface + +The `ReadableStreamInterface` is responsible for providing an interface for +read-only streams and the readable side of duplex streams. + +Besides defining a few methods, this interface also implements the +`EventEmitterInterface` which allows you to react to certain events. + +The event callback functions MUST be a valid `callable` that obeys strict +parameter definitions and MUST accept event parameters exactly as documented. +The event callback functions MUST NOT throw an `Exception`. +The return value of the event callback functions will be ignored and has no +effect, so for performance reasons you're recommended to not return any +excessive data structures. + +Every implementation of this interface MUST follow these event semantics in +order to be considered a well-behaving stream. + +> Note that higher-level implementations of this interface may choose to + define additional events with dedicated semantics not defined as part of + this low-level stream specification. Conformance with these event semantics + is out of scope for this interface, so you may also have to refer to the + documentation of such a higher-level implementation. + +#### data event + +The `data` event will be emitted whenever some data was read/received +from this source stream. +The event receives a single mixed argument for incoming data. + +```php +$stream->on('data', function ($data) { + echo $data; +}); +``` + +This event MAY be emitted any number of times, which may be zero times if +this stream does not send any data at all. +It SHOULD not be emitted after an `end` or `close` event. + +The given `$data` argument may be of mixed type, but it's usually +recommended it SHOULD be a `string` value or MAY use a type that allows +representation as a `string` for maximum compatibility. + +Many common streams (such as a TCP/IP connection or a file-based stream) +will emit the raw (binary) payload data that is received over the wire as +chunks of `string` values. + +Due to the stream-based nature of this, the sender may send any number +of chunks with varying sizes. There are no guarantees that these chunks +will be received with the exact same framing the sender intended to send. +In other words, many lower-level protocols (such as TCP/IP) transfer the +data in chunks that may be anywhere between single-byte values to several +dozens of kilobytes. You may want to apply a higher-level protocol to +these low-level data chunks in order to achieve proper message framing. + +#### end event + +The `end` event will be emitted once the source stream has successfully +reached the end of the stream (EOF). + +```php +$stream->on('end', function () { + echo 'END'; +}); +``` + +This event SHOULD be emitted once or never at all, depending on whether +a successful end was detected. +It SHOULD NOT be emitted after a previous `end` or `close` event. +It MUST NOT be emitted if the stream closes due to a non-successful +end, such as after a previous `error` event. + +After the stream is ended, it MUST switch to non-readable mode, +see also `isReadable()`. + +This event will only be emitted if the *end* was reached successfully, +not if the stream was interrupted by an unrecoverable error or explicitly +closed. Not all streams know this concept of a "successful end". +Many use-cases involve detecting when the stream closes (terminates) +instead, in this case you should use the `close` event. +After the stream emits an `end` event, it SHOULD usually be followed by a +`close` event. + +Many common streams (such as a TCP/IP connection or a file-based stream) +will emit this event if either the remote side closes the connection or +a file handle was successfully read until reaching its end (EOF). + +Note that this event should not be confused with the `end()` method. +This event defines a successful end *reading* from a source stream, while +the `end()` method defines *writing* a successful end to a destination +stream. + +#### error event + +The `error` event will be emitted once a fatal error occurs, usually while +trying to read from this stream. +The event receives a single `Exception` argument for the error instance. + +```php +$server->on('error', function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); +``` + +This event SHOULD be emitted once the stream detects a fatal error, such +as a fatal transmission error or after an unexpected `data` or premature +`end` event. +It SHOULD NOT be emitted after a previous `error`, `end` or `close` event. +It MUST NOT be emitted if this is not a fatal error condition, such as +a temporary network issue that did not cause any data to be lost. + +After the stream errors, it MUST close the stream and SHOULD thus be +followed by a `close` event and then switch to non-readable mode, see +also `close()` and `isReadable()`. + +Many common streams (such as a TCP/IP connection or a file-based stream) +only deal with data transmission and do not make assumption about data +boundaries (such as unexpected `data` or premature `end` events). +In other words, many lower-level protocols (such as TCP/IP) may choose +to only emit this for a fatal transmission error once and will then +close (terminate) the stream in response. + +If this stream is a `DuplexStreamInterface`, you should also notice +how the writable side of the stream also implements an `error` event. +In other words, an error may occur while either reading or writing the +stream which should result in the same error processing. + +#### close event + +The `close` event will be emitted once the stream closes (terminates). + +```php +$stream->on('close', function () { + echo 'CLOSED'; +}); +``` + +This event SHOULD be emitted once or never at all, depending on whether +the stream ever terminates. +It SHOULD NOT be emitted after a previous `close` event. + +After the stream is closed, it MUST switch to non-readable mode, +see also `isReadable()`. + +Unlike the `end` event, this event SHOULD be emitted whenever the stream +closes, irrespective of whether this happens implicitly due to an +unrecoverable error or explicitly when either side closes the stream. +If you only want to detect a *successful* end, you should use the `end` +event instead. + +Many common streams (such as a TCP/IP connection or a file-based stream) +will likely choose to emit this event after reading a *successful* `end` +event or after a fatal transmission `error` event. + +If this stream is a `DuplexStreamInterface`, you should also notice +how the writable side of the stream also implements a `close` event. +In other words, after receiving this event, the stream MUST switch into +non-writable AND non-readable mode, see also `isWritable()`. +Note that this event should not be confused with the `end` event. + +#### isReadable() + +The `isReadable(): bool` method can be used to +check whether this stream is in a readable state (not closed already). + +This method can be used to check if the stream still accepts incoming +data events or if it is ended or closed already. +Once the stream is non-readable, no further `data` or `end` events SHOULD +be emitted. + +```php +assert($stream->isReadable() === false); + +$stream->on('data', assertNeverCalled()); +$stream->on('end', assertNeverCalled()); +``` + +A successfully opened stream always MUST start in readable mode. + +Once the stream ends or closes, it MUST switch to non-readable mode. +This can happen any time, explicitly through `close()` or +implicitly due to a remote close or an unrecoverable transmission error. +Once a stream has switched to non-readable mode, it MUST NOT transition +back to readable mode. + +If this stream is a `DuplexStreamInterface`, you should also notice +how the writable side of the stream also implements an `isWritable()` +method. Unless this is a half-open duplex stream, they SHOULD usually +have the same return value. + +#### pause() + +The `pause(): void` method can be used to +pause reading incoming data events. + +Removes the data source file descriptor from the event loop. This +allows you to throttle incoming data. + +Unless otherwise noted, a successfully opened stream SHOULD NOT start +in paused state. + +Once the stream is paused, no futher `data` or `end` events SHOULD +be emitted. + +```php +$stream->pause(); + +$stream->on('data', assertShouldNeverCalled()); +$stream->on('end', assertShouldNeverCalled()); +``` + +This method is advisory-only, though generally not recommended, the +stream MAY continue emitting `data` events. + +You can continue processing events by calling `resume()` again. + +Note that both methods can be called any number of times, in particular +calling `pause()` more than once SHOULD NOT have any effect. + +See also `resume()`. + +#### resume() + +The `resume(): void` method can be used to +resume reading incoming data events. + +Re-attach the data source after a previous `pause()`. + +```php +$stream->pause(); + +$loop->addTimer(1.0, function () use ($stream) { + $stream->resume(); +}); +``` + +Note that both methods can be called any number of times, in particular +calling `resume()` without a prior `pause()` SHOULD NOT have any effect. + +See also `pause()`. + +#### pipe() + +The `pipe(WritableStreamInterface $dest, array $options = [])` method can be used to +pipe all the data from this readable source into the given writable destination. + +Automatically sends all incoming data to the destination. +Automatically throttles the source based on what the destination can handle. + +```php +$source->pipe($dest); +``` + +Similarly, you can also pipe an instance implementing `DuplexStreamInterface` +into itself in order to write back all the data that is received. +This may be a useful feature for a TCP/IP echo service: + +```php +$connection->pipe($connection); +``` + +This method returns the destination stream as-is, which can be used to +set up chains of piped streams: + +```php +$source->pipe($decodeGzip)->pipe($filterBadWords)->pipe($dest); +``` + +By default, this will call `end()` on the destination stream once the +source stream emits an `end` event. This can be disabled like this: + +```php +$source->pipe($dest, array('end' => false)); +``` + +Note that this only applies to the `end` event. +If an `error` or explicit `close` event happens on the source stream, +you'll have to manually close the destination stream: + +```php +$source->pipe($dest); +$source->on('close', function () use ($dest) { + $dest->end('BYE!'); +}); +``` + +If the source stream is not readable (closed state), then this is a NO-OP. + +```php +$source->close(); +$source->pipe($dest); // NO-OP +``` + +If the destinantion stream is not writable (closed state), then this will simply +throttle (pause) the source stream: + +```php +$dest->close(); +$source->pipe($dest); // calls $source->pause() +``` + +Similarly, if the destination stream is closed while the pipe is still +active, it will also throttle (pause) the source stream: + +```php +$source->pipe($dest); +$dest->close(); // calls $source->pause() +``` + +Once the pipe is set up successfully, the destination stream MUST emit +a `pipe` event with this source stream an event argument. + +#### close() + +The `close(): void` method can be used to +close the stream (forcefully). + +This method can be used to (forcefully) close the stream. + +```php +$stream->close(); +``` + +Once the stream is closed, it SHOULD emit a `close` event. +Note that this event SHOULD NOT be emitted more than once, in particular +if this method is called multiple times. + +After calling this method, the stream MUST switch into a non-readable +mode, see also `isReadable()`. +This means that no further `data` or `end` events SHOULD be emitted. + +```php +$stream->close(); +assert($stream->isReadable() === false); + +$stream->on('data', assertNeverCalled()); +$stream->on('end', assertNeverCalled()); +``` + +If this stream is a `DuplexStreamInterface`, you should also notice +how the writable side of the stream also implements a `close()` method. +In other words, after calling this method, the stream MUST switch into +non-writable AND non-readable mode, see also `isWritable()`. +Note that this method should not be confused with the `end()` method. + +### WritableStreamInterface + +The `WritableStreamInterface` is responsible for providing an interface for +write-only streams and the writable side of duplex streams. + +Besides defining a few methods, this interface also implements the +`EventEmitterInterface` which allows you to react to certain events. + +The event callback functions MUST be a valid `callable` that obeys strict +parameter definitions and MUST accept event parameters exactly as documented. +The event callback functions MUST NOT throw an `Exception`. +The return value of the event callback functions will be ignored and has no +effect, so for performance reasons you're recommended to not return any +excessive data structures. + +Every implementation of this interface MUST follow these event semantics in +order to be considered a well-behaving stream. + +> Note that higher-level implementations of this interface may choose to + define additional events with dedicated semantics not defined as part of + this low-level stream specification. Conformance with these event semantics + is out of scope for this interface, so you may also have to refer to the + documentation of such a higher-level implementation. + +#### drain event + +The `drain` event will be emitted whenever the write buffer became full +previously and is now ready to accept more data. + +```php +$stream->on('drain', function () use ($stream) { + echo 'Stream is now ready to accept more data'; +}); +``` + +This event SHOULD be emitted once every time the buffer became full +previously and is now ready to accept more data. +In other words, this event MAY be emitted any number of times, which may +be zero times if the buffer never became full in the first place. +This event SHOULD NOT be emitted if the buffer has not become full +previously. + +This event is mostly used internally, see also `write()` for more details. + +#### pipe event + +The `pipe` event will be emitted whenever a readable stream is `pipe()`d +into this stream. +The event receives a single `ReadableStreamInterface` argument for the +source stream. + +```php +$stream->on('pipe', function (ReadableStreamInterface $source) use ($stream) { + echo 'Now receiving piped data'; + + // explicitly close target if source emits an error + $source->on('error', function () use ($stream) { + $stream->close(); + }); +}); + +$source->pipe($stream); +``` + +This event MUST be emitted once for each readable stream that is +successfully piped into this destination stream. +In other words, this event MAY be emitted any number of times, which may +be zero times if no stream is ever piped into this stream. +This event MUST NOT be emitted if either the source is not readable +(closed already) or this destination is not writable (closed already). + +This event is mostly used internally, see also `pipe()` for more details. + +#### error event + +The `error` event will be emitted once a fatal error occurs, usually while +trying to write to this stream. +The event receives a single `Exception` argument for the error instance. + +```php +$stream->on('error', function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); +``` + +This event SHOULD be emitted once the stream detects a fatal error, such +as a fatal transmission error. +It SHOULD NOT be emitted after a previous `error` or `close` event. +It MUST NOT be emitted if this is not a fatal error condition, such as +a temporary network issue that did not cause any data to be lost. + +After the stream errors, it MUST close the stream and SHOULD thus be +followed by a `close` event and then switch to non-writable mode, see +also `close()` and `isWritable()`. + +Many common streams (such as a TCP/IP connection or a file-based stream) +only deal with data transmission and may choose +to only emit this for a fatal transmission error once and will then +close (terminate) the stream in response. + +If this stream is a `DuplexStreamInterface`, you should also notice +how the readable side of the stream also implements an `error` event. +In other words, an error may occur while either reading or writing the +stream which should result in the same error processing. + +#### close event + +The `close` event will be emitted once the stream closes (terminates). + +```php +$stream->on('close', function () { + echo 'CLOSED'; +}); +``` + +This event SHOULD be emitted once or never at all, depending on whether +the stream ever terminates. +It SHOULD NOT be emitted after a previous `close` event. + +After the stream is closed, it MUST switch to non-writable mode, +see also `isWritable()`. + +This event SHOULD be emitted whenever the stream closes, irrespective of +whether this happens implicitly due to an unrecoverable error or +explicitly when either side closes the stream. + +Many common streams (such as a TCP/IP connection or a file-based stream) +will likely choose to emit this event after flushing the buffer from +the `end()` method, after receiving a *successful* `end` event or after +a fatal transmission `error` event. + +If this stream is a `DuplexStreamInterface`, you should also notice +how the readable side of the stream also implements a `close` event. +In other words, after receiving this event, the stream MUST switch into +non-writable AND non-readable mode, see also `isReadable()`. +Note that this event should not be confused with the `end` event. + +#### isWritable() + +The `isWritable(): bool` method can be used to +check whether this stream is in a writable state (not closed already). + +This method can be used to check if the stream still accepts writing +any data or if it is ended or closed already. +Writing any data to a non-writable stream is a NO-OP: + +```php +assert($stream->isWritable() === false); + +$stream->write('end'); // NO-OP +$stream->end('end'); // NO-OP +``` + +A successfully opened stream always MUST start in writable mode. + +Once the stream ends or closes, it MUST switch to non-writable mode. +This can happen any time, explicitly through `end()` or `close()` or +implicitly due to a remote close or an unrecoverable transmission error. +Once a stream has switched to non-writable mode, it MUST NOT transition +back to writable mode. + +If this stream is a `DuplexStreamInterface`, you should also notice +how the readable side of the stream also implements an `isReadable()` +method. Unless this is a half-open duplex stream, they SHOULD usually +have the same return value. + +#### write() + +The `write(mixed $data): bool` method can be used to +write some data into the stream. + +A successful write MUST be confirmed with a boolean `true`, which means +that either the data was written (flushed) immediately or is buffered and +scheduled for a future write. Note that this interface gives you no +control over explicitly flushing the buffered data, as finding the +appropriate time for this is beyond the scope of this interface and left +up to the implementation of this interface. + +Many common streams (such as a TCP/IP connection or file-based stream) +may choose to buffer all given data and schedule a future flush by using +an underlying EventLoop to check when the resource is actually writable. + +If a stream cannot handle writing (or flushing) the data, it SHOULD emit +an `error` event and MAY `close()` the stream if it can not recover from +this error. + +If the internal buffer is full after adding `$data`, then `write()` +SHOULD return `false`, indicating that the caller should stop sending +data until the buffer drains. +The stream SHOULD send a `drain` event once the buffer is ready to accept +more data. + +Similarly, if the the stream is not writable (already in a closed state) +it MUST NOT process the given `$data` and SHOULD return `false`, +indicating that the caller should stop sending data. + +The given `$data` argument MAY be of mixed type, but it's usually +recommended it SHOULD be a `string` value or MAY use a type that allows +representation as a `string` for maximum compatibility. + +Many common streams (such as a TCP/IP connection or a file-based stream) +will only accept the raw (binary) payload data that is transferred over +the wire as chunks of `string` values. + +Due to the stream-based nature of this, the sender may send any number +of chunks with varying sizes. There are no guarantees that these chunks +will be received with the exact same framing the sender intended to send. +In other words, many lower-level protocols (such as TCP/IP) transfer the +data in chunks that may be anywhere between single-byte values to several +dozens of kilobytes. You may want to apply a higher-level protocol to +these low-level data chunks in order to achieve proper message framing. + +#### end() + +The `end(mixed $data = null): void` method can be used to +successfully end the stream (after optionally sending some final data). + +This method can be used to successfully end the stream, i.e. close +the stream after sending out all data that is currently buffered. + +```php +$stream->write('hello'); +$stream->write('world'); +$stream->end(); +``` + +If there's no data currently buffered and nothing to be flushed, then +this method MAY `close()` the stream immediately. + +If there's still data in the buffer that needs to be flushed first, then +this method SHOULD try to write out this data and only then `close()` +the stream. +Once the stream is closed, it SHOULD emit a `close` event. + +Note that this interface gives you no control over explicitly flushing +the buffered data, as finding the appropriate time for this is beyond the +scope of this interface and left up to the implementation of this +interface. + +Many common streams (such as a TCP/IP connection or file-based stream) +may choose to buffer all given data and schedule a future flush by using +an underlying EventLoop to check when the resource is actually writable. + +You can optionally pass some final data that is written to the stream +before ending the stream. If a non-`null` value is given as `$data`, then +this method will behave just like calling `write($data)` before ending +with no data. + +```php +// shorter version +$stream->end('bye'); + +// same as longer version +$stream->write('bye'); +$stream->end(); +``` + +After calling this method, the stream MUST switch into a non-writable +mode, see also `isWritable()`. +This means that no further writes are possible, so any additional +`write()` or `end()` calls have no effect. + +```php +$stream->end(); +assert($stream->isWritable() === false); + +$stream->write('nope'); // NO-OP +$stream->end(); // NO-OP +``` + +If this stream is a `DuplexStreamInterface`, calling this method SHOULD +also end its readable side, unless the stream supports half-open mode. +In other words, after calling this method, these streams SHOULD switch +into non-writable AND non-readable mode, see also `isReadable()`. +This implies that in this case, the stream SHOULD NOT emit any `data` +or `end` events anymore. +Streams MAY choose to use the `pause()` method logic for this, but +special care may have to be taken to ensure a following call to the +`resume()` method SHOULD NOT continue emitting readable events. + +Note that this method should not be confused with the `close()` method. + +#### close() + +The `close(): void` method can be used to +close the stream (forcefully). + +This method can be used to forcefully close the stream, i.e. close +the stream without waiting for any buffered data to be flushed. +If there's still data in the buffer, this data SHOULD be discarded. + +```php +$stream->close(); +``` + +Once the stream is closed, it SHOULD emit a `close` event. +Note that this event SHOULD NOT be emitted more than once, in particular +if this method is called multiple times. + +After calling this method, the stream MUST switch into a non-writable +mode, see also `isWritable()`. +This means that no further writes are possible, so any additional +`write()` or `end()` calls have no effect. + +```php +$stream->close(); +assert($stream->isWritable() === false); + +$stream->write('nope'); // NO-OP +$stream->end(); // NO-OP +``` + +Note that this method should not be confused with the `end()` method. +Unlike the `end()` method, this method does not take care of any existing +buffers and simply discards any buffer contents. +Likewise, this method may also be called after calling `end()` on a +stream in order to stop waiting for the stream to flush its final data. + +```php +$stream->end(); +$loop->addTimer(1.0, function () use ($stream) { + $stream->close(); +}); +``` + +If this stream is a `DuplexStreamInterface`, you should also notice +how the readable side of the stream also implements a `close()` method. +In other words, after calling this method, the stream MUST switch into +non-writable AND non-readable mode, see also `isReadable()`. + +### DuplexStreamInterface + +The `DuplexStreamInterface` is responsible for providing an interface for +duplex streams (both readable and writable). + +It builds on top of the existing interfaces for readable and writable streams +and follows the exact same method and event semantics. +If you're new to this concept, you should look into the +`ReadableStreamInterface` and `WritableStreamInterface` first. + +Besides defining a few methods, this interface also implements the +`EventEmitterInterface` which allows you to react to the same events defined +on the `ReadbleStreamInterface` and `WritableStreamInterface`. + +The event callback functions MUST be a valid `callable` that obeys strict +parameter definitions and MUST accept event parameters exactly as documented. +The event callback functions MUST NOT throw an `Exception`. +The return value of the event callback functions will be ignored and has no +effect, so for performance reasons you're recommended to not return any +excessive data structures. + +Every implementation of this interface MUST follow these event semantics in +order to be considered a well-behaving stream. + +> Note that higher-level implementations of this interface may choose to + define additional events with dedicated semantics not defined as part of + this low-level stream specification. Conformance with these event semantics + is out of scope for this interface, so you may also have to refer to the + documentation of such a higher-level implementation. + +See also [`ReadableStreamInterface`](#readablestreaminterface) and +[`WritableStreamInterface`](#writablestreaminterface) for more details. + +## Creating streams + +ReactPHP uses the concept of "streams" throughout its ecosystem, so that +many higher-level consumers of this package only deal with +[stream usage](#stream-usage). +This implies that stream instances are most often created within some +higher-level components and many consumers never actually have to deal with +creating a stream instance. + +* Use [react/socket](https://github.com/reactphp/socket) + if you want to accept incoming or establish outgoing plaintext TCP/IP or + secure TLS socket connection streams. +* Use [react/http](https://github.com/reactphp/http) + if you want to receive an incoming HTTP request body streams. +* Use [react/child-process](https://github.com/reactphp/child-process) + if you want to communicate with child processes via process pipes such as + STDIN, STDOUT, STDERR etc. +* Use experimental [react/filesystem](https://github.com/reactphp/filesystem) + if you want to read from / write to the filesystem. +* See also the last chapter for [more real-world applications](#more). + +However, if you are writing a lower-level component or want to create a stream +instance from a stream resource, then the following chapter is for you. + +> Note that the following examples use `fopen()` and `stream_socket_client()` + for illustration purposes only. + These functions SHOULD NOT be used in a truly async program because each call + may take several seconds to complete and would block the EventLoop otherwise. + Additionally, the `fopen()` call will return a file handle on some platforms + which may or may not be supported by all EventLoop implementations. + As an alternative, you may want to use higher-level libraries listed above. + +### ReadableResourceStream + +The `ReadableResourceStream` is a concrete implementation of the +[`ReadableStreamInterface`](#readablestreaminterface) for PHP's stream resources. + +This can be used to represent a read-only resource like a file stream opened in +readable mode or a stream such as `STDIN`: + +```php +$stream = new ReadableResourceStream(STDIN, $loop); +$stream->on('data', function ($chunk) { + echo $chunk; +}); +$stream->on('end', function () { + echo 'END'; +}); +``` + +See also [`ReadableStreamInterface`](#readablestreaminterface) for more details. + +The first parameter given to the constructor MUST be a valid stream resource +that is opened in reading mode (e.g. `fopen()` mode `r`). +Otherwise, it will throw an `InvalidArgumentException`: + +```php +// throws InvalidArgumentException +$stream = new ReadableResourceStream(false, $loop); +``` + +See also the [`DuplexResourceStream`](#readableresourcestream) for read-and-write +stream resources otherwise. + +Internally, this class tries to enable non-blocking mode on the stream resource +which may not be supported for all stream resources. +Most notably, this is not supported by pipes on Windows (STDIN etc.). +If this fails, it will throw a `RuntimeException`: + +```php +// throws RuntimeException on Windows +$stream = new ReadableResourceStream(STDIN, $loop); +``` + +Once the constructor is called with a valid stream resource, this class will +take care of the underlying stream resource. +You SHOULD only use its public API and SHOULD NOT interfere with the underlying +stream resource manually. + +This class takes an optional `int|null $readChunkSize` parameter that controls +the maximum buffer size in bytes to read at once from the stream. +You can use a `null` value here in order to apply its default value. +This value SHOULD NOT be changed unless you know what you're doing. +This can be a positive number which means that up to X bytes will be read +at once from the underlying stream resource. Note that the actual number +of bytes read may be lower if the stream resource has less than X bytes +currently available. +This can be `-1` which means "read everything available" from the +underlying stream resource. +This should read until the stream resource is not readable anymore +(i.e. underlying buffer drained), note that this does not neccessarily +mean it reached EOF. + +```php +$stream = new ReadableResourceStream(STDIN, $loop, 8192); +``` + +> PHP bug warning: If the PHP process has explicitly been started without a + `STDIN` stream, then trying to read from `STDIN` may return data from + another stream resource. This does not happen if you start this with an empty + stream like `php test.php < /dev/null` instead of `php test.php <&-`. + See [#81](https://github.com/reactphp/stream/issues/81) for more details. + +### WritableResourceStream + +The `WritableResourceStream` is a concrete implementation of the +[`WritableStreamInterface`](#writablestreaminterface) for PHP's stream resources. + +This can be used to represent a write-only resource like a file stream opened in +writable mode or a stream such as `STDOUT` or `STDERR`: + +```php +$stream = new WritableResourceStream(STDOUT, $loop); +$stream->write('hello!'); +$stream->end(); +``` + +See also [`WritableStreamInterface`](#writablestreaminterface) for more details. + +The first parameter given to the constructor MUST be a valid stream resource +that is opened for writing. +Otherwise, it will throw an `InvalidArgumentException`: + +```php +// throws InvalidArgumentException +$stream = new WritableResourceStream(false, $loop); +``` + +See also the [`DuplexResourceStream`](#readableresourcestream) for read-and-write +stream resources otherwise. + +Internally, this class tries to enable non-blocking mode on the stream resource +which may not be supported for all stream resources. +Most notably, this is not supported by pipes on Windows (STDOUT, STDERR etc.). +If this fails, it will throw a `RuntimeException`: + +```php +// throws RuntimeException on Windows +$stream = new WritableResourceStream(STDOUT, $loop); +``` + +Once the constructor is called with a valid stream resource, this class will +take care of the underlying stream resource. +You SHOULD only use its public API and SHOULD NOT interfere with the underlying +stream resource manually. + +Any `write()` calls to this class will not be performed instantly, but will +be performed asynchronously, once the EventLoop reports the stream resource is +ready to accept data. +For this, it uses an in-memory buffer string to collect all outstanding writes. +This buffer has a soft-limit applied which defines how much data it is willing +to accept before the caller SHOULD stop sending further data. + +This class takes an optional `int|null $writeBufferSoftLimit` parameter that controls +this maximum buffer size in bytes. +You can use a `null` value here in order to apply its default value. +This value SHOULD NOT be changed unless you know what you're doing. + +```php +$stream = new WritableResourceStream(STDOUT, $loop, 8192); +``` + +This class takes an optional `int|null $writeChunkSize` parameter that controls +this maximum buffer size in bytes to write at once to the stream. +You can use a `null` value here in order to apply its default value. +This value SHOULD NOT be changed unless you know what you're doing. +This can be a positive number which means that up to X bytes will be written +at once to the underlying stream resource. Note that the actual number +of bytes written may be lower if the stream resource has less than X bytes +currently available. +This can be `-1` which means "write everything available" to the +underlying stream resource. + +```php +$stream = new WritableResourceStream(STDOUT, $loop, null, 8192); +``` + +See also [`write()`](#write) for more details. + +### DuplexResourceStream + +The `DuplexResourceStream` is a concrete implementation of the +[`DuplexStreamInterface`](#duplexstreaminterface) for PHP's stream resources. + +This can be used to represent a read-and-write resource like a file stream opened +in read and write mode mode or a stream such as a TCP/IP connection: + +```php +$conn = stream_socket_client('tcp://google.com:80'); +$stream = new DuplexResourceStream($conn, $loop); +$stream->write('hello!'); +$stream->end(); +``` + +See also [`DuplexStreamInterface`](#duplexstreaminterface) for more details. + +The first parameter given to the constructor MUST be a valid stream resource +that is opened for reading *and* writing. +Otherwise, it will throw an `InvalidArgumentException`: + +```php +// throws InvalidArgumentException +$stream = new DuplexResourceStream(false, $loop); +``` + +See also the [`ReadableResourceStream`](#readableresourcestream) for read-only +and the [`WritableResourceStream`](#writableresourcestream) for write-only +stream resources otherwise. + +Internally, this class tries to enable non-blocking mode on the stream resource +which may not be supported for all stream resources. +Most notably, this is not supported by pipes on Windows (STDOUT, STDERR etc.). +If this fails, it will throw a `RuntimeException`: + +```php +// throws RuntimeException on Windows +$stream = new DuplexResourceStream(STDOUT, $loop); +``` + +Once the constructor is called with a valid stream resource, this class will +take care of the underlying stream resource. +You SHOULD only use its public API and SHOULD NOT interfere with the underlying +stream resource manually. + +This class takes an optional `int|null $readChunkSize` parameter that controls +the maximum buffer size in bytes to read at once from the stream. +You can use a `null` value here in order to apply its default value. +This value SHOULD NOT be changed unless you know what you're doing. +This can be a positive number which means that up to X bytes will be read +at once from the underlying stream resource. Note that the actual number +of bytes read may be lower if the stream resource has less than X bytes +currently available. +This can be `-1` which means "read everything available" from the +underlying stream resource. +This should read until the stream resource is not readable anymore +(i.e. underlying buffer drained), note that this does not neccessarily +mean it reached EOF. + +```php +$conn = stream_socket_client('tcp://google.com:80'); +$stream = new DuplexResourceStream($conn, $loop, 8192); +``` + +Any `write()` calls to this class will not be performed instantly, but will +be performed asynchronously, once the EventLoop reports the stream resource is +ready to accept data. +For this, it uses an in-memory buffer string to collect all outstanding writes. +This buffer has a soft-limit applied which defines how much data it is willing +to accept before the caller SHOULD stop sending further data. + +This class takes another optional `WritableStreamInterface|null $buffer` parameter +that controls this write behavior of this stream. +You can use a `null` value here in order to apply its default value. +This value SHOULD NOT be changed unless you know what you're doing. + +If you want to change the write buffer soft limit, you can pass an instance of +[`WritableResourceStream`](#writableresourcestream) like this: + +```php +$conn = stream_socket_client('tcp://google.com:80'); +$buffer = new WritableResourceStream($conn, $loop, 8192); +$stream = new DuplexResourceStream($conn, $loop, null, $buffer); +``` + +See also [`WritableResourceStream`](#writableresourcestream) for more details. + +### ThroughStream + +The `ThroughStream` implements the +[`DuplexStreamInterface`](#duplexstreaminterface) and will simply pass any data +you write to it through to its readable end. + +```php +$through = new ThroughStream(); +$through->on('data', $this->expectCallableOnceWith('hello')); + +$through->write('hello'); +``` + +Similarly, the [`end()` method](#end) will end the stream and emit an +[`end` event](#end-event) and then [`close()`](#close-1) the stream. +The [`close()` method](#close-1) will close the stream and emit a +[`close` event](#close-event). +Accordingly, this is can also be used in a [`pipe()`](#pipe) context like this: + +```php +$through = new ThroughStream(); +$source->pipe($through)->pipe($dest); +``` + +Optionally, its constructor accepts any callable function which will then be +used to *filter* any data written to it. This function receives a single data +argument as passed to the writable side and must return the data as it will be +passed to its readable end: + +```php +$through = new ThroughStream('strtoupper'); +$source->pipe($through)->pipe($dest); +``` + +Note that this class makes no assumptions about any data types. This can be +used to convert data, for example for transforming any structured data into +a newline-delimited JSON (NDJSON) stream like this: + +```php +$through = new ThroughStream(function ($data) { + return json_encode($data) . PHP_EOL; +}); +$through->on('data', $this->expectCallableOnceWith("[2, true]\n")); + +$through->write(array(2, true)); +``` + +The callback function is allowed to throw an `Exception`. In this case, +the stream will emit an `error` event and then [`close()`](#close-1) the stream. + +```php +$through = new ThroughStream(function ($data) { + if (!is_string($data)) { + throw new \UnexpectedValueException('Only strings allowed'); + } + return $data; +}); +$through->on('error', $this->expectCallableOnce())); +$through->on('close', $this->expectCallableOnce())); +$through->on('data', $this->expectCallableNever())); + +$through->write(2); +``` + +### CompositeStream + +The `CompositeStream` implements the +[`DuplexStreamInterface`](#duplexstreaminterface) and can be used to create a +single duplex stream from two individual streams implementing +[`ReadableStreamInterface`](#readablestreaminterface) and +[`WritableStreamInterface`](#writablestreaminterface) respectively. + +This is useful for some APIs which may require a single +[`DuplexStreamInterface`](#duplexstreaminterface) or simply because it's often +more convenient to work with a single stream instance like this: + +```php +$stdin = new ReadableResourceStream(STDIN, $loop); +$stdout = new WritableResourceStream(STDOUT, $loop); + +$stdio = new CompositeStream($stdin, $stdout); + +$stdio->on('data', function ($chunk) use ($stdio) { + $stdio->write('You said: ' . $chunk); +}); +``` + +This is a well-behaving stream which forwards all stream events from the +underlying streams and forwards all streams calls to the underlying streams. + +If you `write()` to the duplex stream, it will simply `write()` to the +writable side and return its status. + +If you `end()` the duplex stream, it will `end()` the writable side and will +`pause()` the readable side. + +If you `close()` the duplex stream, both input streams will be closed. +If either of the two input streams emits a `close` event, the duplex stream +will also close. +If either of the two input streams is already closed while constructing the +duplex stream, it will `close()` the other side and return a closed stream. + +## Usage + +The following example can be used to pipe the contents of a source file into +a destination file without having to ever read the whole file into memory: + +```php +$loop = new React\EventLoop\StreamSelectLoop; + +$source = new React\Stream\ReadableResourceStream(fopen('source.txt', 'r'), $loop); +$dest = new React\Stream\WritableResourceStream(fopen('destination.txt', 'w'), $loop); + +$source->pipe($dest); + +$loop->run(); +``` + +> Note that this example uses `fopen()` for illustration purposes only. + This should not be used in a truly async program because the filesystem is + inherently blocking and each call could potentially take several seconds. + See also [creating streams](#creating-streams) for more sophisticated + examples. + +## Install + +The recommended way to install this library is [through Composer](https://getcomposer.org). +[New to Composer?](https://getcomposer.org/doc/00-intro.md) + +This will install the latest supported version: + +```bash +$ composer require react/stream:^0.7.7 +``` + +See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. + +This project aims to run on any platform and thus does not require any PHP +extensions and supports running on legacy PHP 5.3 through current PHP 7+ and HHVM. +It's *highly recommended to use PHP 7+* for this project due to its vast +performance improvements. + +## Tests + +To run the test suite, you first need to clone this repo and then install all +dependencies [through Composer](https://getcomposer.org): + +```bash +$ composer install +``` + +To run the test suite, go to the project root and run: + +```bash +$ php vendor/bin/phpunit +``` + +The test suite also contains a number of functional integration tests that rely +on a stable internet connection. +If you do not want to run these, they can simply be skipped like this: + +```bash +$ php vendor/bin/phpunit --exclude-group internet +``` + +## License + +MIT, see [LICENSE file](LICENSE). + +## More + +* See [creating streams](#creating-streams) for more information on how streams + are created in real-world applications. +* See our [users wiki](https://github.com/reactphp/react/wiki/Users) and the + [dependents on Packagist](https://packagist.org/packages/react/stream/dependents) + for a list of packages that use streams in real-world applications. diff --git a/assets/php/vendor/react/stream/composer.json b/assets/php/vendor/react/stream/composer.json new file mode 100644 index 0000000..f6faa66 --- /dev/null +++ b/assets/php/vendor/react/stream/composer.json @@ -0,0 +1,25 @@ +{ + "name": "react/stream", + "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", + "keywords": ["event-driven", "readable", "writable", "stream", "non-blocking", "io", "pipe", "ReactPHP"], + "license": "MIT", + "require": { + "php": ">=5.3.8", + "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3.5", + "evenement/evenement": "^3.0 || ^2.0 || ^1.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.4 || ^5.7 || ^4.8.35", + "clue/stream-filter": "~1.2" + }, + "autoload": { + "psr-4": { + "React\\Stream\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "React\\Tests\\Stream\\": "tests" + } + } +} diff --git a/assets/php/vendor/react/stream/examples/01-http.php b/assets/php/vendor/react/stream/examples/01-http.php new file mode 100644 index 0000000..3687f7c --- /dev/null +++ b/assets/php/vendor/react/stream/examples/01-http.php @@ -0,0 +1,40 @@ +<?php + +// Simple plaintext HTTP client example (for illustration purposes only). +// This shows how a plaintext TCP/IP connection is established to then send an +// application level protocol message (HTTP). +// Real applications should use react/http-client instead! +// +// This simple example only accepts an optional host parameter to send the +// request to. +// +// $ php examples/01-http.php +// $ php examples/01-http.php reactphp.org + +use React\EventLoop\Factory; +use React\Stream\DuplexResourceStream; + +require __DIR__ . '/../vendor/autoload.php'; + +$host = isset($argv[1]) ? $argv[1] : 'www.google.com'; + +// connect to tcp://www.google.com:80 (blocking call!) +// for illustration purposes only, should use react/http-client or react/socket instead! +$resource = stream_socket_client('tcp://' . $host . ':80'); +if (!$resource) { + exit(1); +} + +$loop = Factory::create(); +$stream = new DuplexResourceStream($resource, $loop); + +$stream->on('data', function ($chunk) { + echo $chunk; +}); +$stream->on('close', function () { + echo '[CLOSED]' . PHP_EOL; +}); + +$stream->write("GET / HTTP/1.0\r\nHost: $host\r\n\r\n"); + +$loop->run(); diff --git a/assets/php/vendor/react/stream/examples/02-https.php b/assets/php/vendor/react/stream/examples/02-https.php new file mode 100644 index 0000000..163f7c8 --- /dev/null +++ b/assets/php/vendor/react/stream/examples/02-https.php @@ -0,0 +1,40 @@ +<?php + +// Simple secure HTTPS client example (for illustration purposes only). +// This shows how a secure TLS connection is established to then send an +// application level protocol message (HTTP). +// Real applications should use react/http-client instead! +// +// This simple example only accepts an optional host parameter to send the +// request to. +// +// $ php examples/02-https.php +// $ php examples/02-https.php reactphp.org + +use React\EventLoop\Factory; +use React\Stream\DuplexResourceStream; + +require __DIR__ . '/../vendor/autoload.php'; + +$host = isset($argv[1]) ? $argv[1] : 'www.google.com'; + +// connect to tls://www.google.com:443 (blocking call!) +// for illustration purposes only, should use react/http-client or react/socket instead! +$resource = stream_socket_client('tls://' . $host . ':443'); +if (!$resource) { + exit(1); +} + +$loop = Factory::create(); +$stream = new DuplexResourceStream($resource, $loop); + +$stream->on('data', function ($chunk) { + echo $chunk; +}); +$stream->on('close', function () { + echo '[CLOSED]' . PHP_EOL; +}); + +$stream->write("GET / HTTP/1.0\r\nHost: $host\r\n\r\n"); + +$loop->run(); diff --git a/assets/php/vendor/react/stream/examples/11-cat.php b/assets/php/vendor/react/stream/examples/11-cat.php new file mode 100644 index 0000000..90fadc0 --- /dev/null +++ b/assets/php/vendor/react/stream/examples/11-cat.php @@ -0,0 +1,28 @@ +<?php + +// Simple example piping everything from STDIN to STDOUT. +// This allows you to output everything you type on your keyboard or to redirect +// the pipes to show contents of files and other streams. +// +// $ php examples/11-cat.php +// $ php examples/11-cat.php < README.md +// $ echo hello | php examples/11-cat.php + +use React\EventLoop\Factory; +use React\Stream\ReadableResourceStream; +use React\Stream\WritableResourceStream; + +require __DIR__ . '/../vendor/autoload.php'; + +if (DIRECTORY_SEPARATOR === '\\') { + fwrite(STDERR, 'Non-blocking console I/O not supported on Microsoft Windows' . PHP_EOL); + exit(1); +} + +$loop = Factory::create(); + +$stdout = new WritableResourceStream(STDOUT, $loop); +$stdin = new ReadableResourceStream(STDIN, $loop); +$stdin->pipe($stdout); + +$loop->run(); diff --git a/assets/php/vendor/react/stream/examples/91-benchmark-throughput.php b/assets/php/vendor/react/stream/examples/91-benchmark-throughput.php new file mode 100644 index 0000000..ecf695c --- /dev/null +++ b/assets/php/vendor/react/stream/examples/91-benchmark-throughput.php @@ -0,0 +1,62 @@ +<?php + +// Benchmark to measure throughput performance piping an input stream to an output stream. +// This allows you to get an idea of how fast stream processing with PHP can be +// and also to play around with differnt types of input and output streams. +// +// This example accepts a number of parameters to control the timeout (-t 1), +// the input file (-i /dev/zero) and the output file (-o /dev/null). +// +// $ php examples/91-benchmark-throughput.php +// $ php examples/91-benchmark-throughput.php -t 10 -o zero.bin +// $ php examples/91-benchmark-throughput.php -t 60 -i zero.bin + +require __DIR__ . '/../vendor/autoload.php'; + +if (DIRECTORY_SEPARATOR === '\\') { + fwrite(STDERR, 'Non-blocking console I/O not supported on Microsoft Windows' . PHP_EOL); + exit(1); +} + +$args = getopt('i:o:t:'); +$if = isset($args['i']) ? $args['i'] : '/dev/zero'; +$of = isset($args['o']) ? $args['o'] : '/dev/null'; +$t = isset($args['t']) ? $args['t'] : 1; + +// passing file descriptors requires mapping paths (https://bugs.php.net/bug.php?id=53465) +$if = str_replace('/dev/fd/', 'php://fd/', $if); +$of = str_replace('/dev/fd/', 'php://fd/', $of); + +$loop = new React\EventLoop\StreamSelectLoop(); + +// setup information stream +$info = new React\Stream\WritableResourceStream(STDERR, $loop); +if (extension_loaded('xdebug')) { + $info->write('NOTICE: The "xdebug" extension is loaded, this has a major impact on performance.' . PHP_EOL); +} +$info->write('piping from ' . $if . ' to ' . $of . ' (for max ' . $t . ' second(s)) ...'. PHP_EOL); + +// setup input and output streams and pipe inbetween +$fh = fopen($if, 'r'); +$in = new React\Stream\ReadableResourceStream($fh, $loop); +$out = new React\Stream\WritableResourceStream(fopen($of, 'w'), $loop); +$in->pipe($out); + +// stop input stream in $t seconds +$start = microtime(true); +$timeout = $loop->addTimer($t, function () use ($in, &$bytes) { + $in->close(); +}); + +// print stream position once stream closes +$in->on('close', function () use ($fh, $start, $loop, $timeout, $info) { + $t = microtime(true) - $start; + $loop->cancelTimer($timeout); + + $bytes = ftell($fh); + + $info->write('read ' . $bytes . ' byte(s) in ' . round($t, 3) . ' second(s) => ' . round($bytes / 1024 / 1024 / $t, 1) . ' MiB/s' . PHP_EOL); + $info->write('peak memory usage of ' . round(memory_get_peak_usage(true) / 1024 / 1024, 1) . ' MiB' . PHP_EOL); +}); + +$loop->run(); diff --git a/assets/php/vendor/react/stream/phpunit.xml.dist b/assets/php/vendor/react/stream/phpunit.xml.dist new file mode 100644 index 0000000..13d3fab --- /dev/null +++ b/assets/php/vendor/react/stream/phpunit.xml.dist @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<phpunit backupGlobals="false" + backupStaticAttributes="false" + colors="true" + convertErrorsToExceptions="true" + convertNoticesToExceptions="true" + convertWarningsToExceptions="true" + processIsolation="false" + stopOnFailure="false" + syntaxCheck="false" + bootstrap="vendor/autoload.php" +> + <testsuites> + <testsuite name="React Test Suite"> + <directory>./tests/</directory> + </testsuite> + </testsuites> + + <filter> + <whitelist> + <directory>./src/</directory> + </whitelist> + </filter> +</phpunit> diff --git a/assets/php/vendor/react/stream/src/CompositeStream.php b/assets/php/vendor/react/stream/src/CompositeStream.php new file mode 100644 index 0000000..153f2a3 --- /dev/null +++ b/assets/php/vendor/react/stream/src/CompositeStream.php @@ -0,0 +1,82 @@ +<?php + +namespace React\Stream; + +use Evenement\EventEmitter; + +final class CompositeStream extends EventEmitter implements DuplexStreamInterface +{ + private $readable; + private $writable; + private $closed = false; + + public function __construct(ReadableStreamInterface $readable, WritableStreamInterface $writable) + { + $this->readable = $readable; + $this->writable = $writable; + + if (!$readable->isReadable() || !$writable->isWritable()) { + return $this->close(); + } + + Util::forwardEvents($this->readable, $this, array('data', 'end', 'error')); + Util::forwardEvents($this->writable, $this, array('drain', 'error', 'pipe')); + + $this->readable->on('close', array($this, 'close')); + $this->writable->on('close', array($this, 'close')); + } + + public function isReadable() + { + return $this->readable->isReadable(); + } + + public function pause() + { + $this->readable->pause(); + } + + public function resume() + { + if (!$this->writable->isWritable()) { + return; + } + + $this->readable->resume(); + } + + public function pipe(WritableStreamInterface $dest, array $options = array()) + { + return Util::pipe($this, $dest, $options); + } + + public function isWritable() + { + return $this->writable->isWritable(); + } + + public function write($data) + { + return $this->writable->write($data); + } + + public function end($data = null) + { + $this->readable->pause(); + $this->writable->end($data); + } + + public function close() + { + if ($this->closed) { + return; + } + + $this->closed = true; + $this->readable->close(); + $this->writable->close(); + + $this->emit('close'); + $this->removeAllListeners(); + } +} diff --git a/assets/php/vendor/react/stream/src/DuplexResourceStream.php b/assets/php/vendor/react/stream/src/DuplexResourceStream.php new file mode 100644 index 0000000..982ebb0 --- /dev/null +++ b/assets/php/vendor/react/stream/src/DuplexResourceStream.php @@ -0,0 +1,224 @@ +<?php + +namespace React\Stream; + +use Evenement\EventEmitter; +use React\EventLoop\LoopInterface; +use InvalidArgumentException; + +final class DuplexResourceStream extends EventEmitter implements DuplexStreamInterface +{ + private $stream; + private $loop; + + /** + * Controls the maximum buffer size in bytes to read at once from the stream. + * + * This can be a positive number which means that up to X bytes will be read + * at once from the underlying stream resource. Note that the actual number + * of bytes read may be lower if the stream resource has less than X bytes + * currently available. + * + * This can be `-1` which means read everything available from the + * underlying stream resource. + * This should read until the stream resource is not readable anymore + * (i.e. underlying buffer drained), note that this does not neccessarily + * mean it reached EOF. + * + * @var int + */ + private $bufferSize; + private $buffer; + + private $readable = true; + private $writable = true; + private $closing = false; + private $listening = false; + + public function __construct($stream, LoopInterface $loop, $readChunkSize = null, WritableStreamInterface $buffer = null) + { + if (!is_resource($stream) || get_resource_type($stream) !== "stream") { + throw new InvalidArgumentException('First parameter must be a valid stream resource'); + } + + // ensure resource is opened for reading and wrting (fopen mode must contain "+") + $meta = stream_get_meta_data($stream); + if (isset($meta['mode']) && $meta['mode'] !== '' && strpos($meta['mode'], '+') === false) { + throw new InvalidArgumentException('Given stream resource is not opened in read and write mode'); + } + + // this class relies on non-blocking I/O in order to not interrupt the event loop + // e.g. pipes on Windows do not support this: https://bugs.php.net/bug.php?id=47918 + if (stream_set_blocking($stream, 0) !== true) { + throw new \RuntimeException('Unable to set stream resource to non-blocking mode'); + } + + // Use unbuffered read operations on the underlying stream resource. + // Reading chunks from the stream may otherwise leave unread bytes in + // PHP's stream buffers which some event loop implementations do not + // trigger events on (edge triggered). + // This does not affect the default event loop implementation (level + // triggered), so we can ignore platforms not supporting this (HHVM). + // Pipe streams (such as STDIN) do not seem to require this and legacy + // PHP versions cause SEGFAULTs on unbuffered pipe streams, so skip this. + if (function_exists('stream_set_read_buffer') && !$this->isLegacyPipe($stream)) { + stream_set_read_buffer($stream, 0); + } + + if ($buffer === null) { + $buffer = new WritableResourceStream($stream, $loop); + } + + $this->stream = $stream; + $this->loop = $loop; + $this->bufferSize = ($readChunkSize === null) ? 65536 : (int)$readChunkSize; + $this->buffer = $buffer; + + $that = $this; + + $this->buffer->on('error', function ($error) use ($that) { + $that->emit('error', array($error)); + }); + + $this->buffer->on('close', array($this, 'close')); + + $this->buffer->on('drain', function () use ($that) { + $that->emit('drain'); + }); + + $this->resume(); + } + + public function isReadable() + { + return $this->readable; + } + + public function isWritable() + { + return $this->writable; + } + + public function pause() + { + if ($this->listening) { + $this->loop->removeReadStream($this->stream); + $this->listening = false; + } + } + + public function resume() + { + if (!$this->listening && $this->readable) { + $this->loop->addReadStream($this->stream, array($this, 'handleData')); + $this->listening = true; + } + } + + public function write($data) + { + if (!$this->writable) { + return false; + } + + return $this->buffer->write($data); + } + + public function close() + { + if (!$this->writable && !$this->closing) { + return; + } + + $this->closing = false; + + $this->readable = false; + $this->writable = false; + + $this->emit('close'); + $this->pause(); + $this->buffer->close(); + $this->removeAllListeners(); + + if (is_resource($this->stream)) { + fclose($this->stream); + } + } + + public function end($data = null) + { + if (!$this->writable) { + return; + } + + $this->closing = true; + + $this->readable = false; + $this->writable = false; + $this->pause(); + + $this->buffer->end($data); + } + + public function pipe(WritableStreamInterface $dest, array $options = array()) + { + return Util::pipe($this, $dest, $options); + } + + /** @internal */ + public function handleData($stream) + { + $error = null; + set_error_handler(function ($errno, $errstr, $errfile, $errline) use (&$error) { + $error = new \ErrorException( + $errstr, + 0, + $errno, + $errfile, + $errline + ); + }); + + $data = stream_get_contents($stream, $this->bufferSize); + + restore_error_handler(); + + if ($error !== null) { + $this->emit('error', array(new \RuntimeException('Unable to read from stream: ' . $error->getMessage(), 0, $error))); + $this->close(); + return; + } + + if ($data !== '') { + $this->emit('data', array($data)); + } else{ + // no data read => we reached the end and close the stream + $this->emit('end'); + $this->close(); + } + } + + /** + * Returns whether this is a pipe resource in a legacy environment + * + * This works around a legacy PHP bug (#61019) that was fixed in PHP 5.4.28+ + * and PHP 5.5.12+ and newer. + * + * @param resource $resource + * @return bool + * @link https://github.com/reactphp/child-process/issues/40 + * + * @codeCoverageIgnore + */ + private function isLegacyPipe($resource) + { + if (PHP_VERSION_ID < 50428 || (PHP_VERSION_ID >= 50500 && PHP_VERSION_ID < 50512)) { + $meta = stream_get_meta_data($resource); + + if (isset($meta['stream_type']) && $meta['stream_type'] === 'STDIO') { + return true; + } + } + return false; + } +} diff --git a/assets/php/vendor/react/stream/src/DuplexStreamInterface.php b/assets/php/vendor/react/stream/src/DuplexStreamInterface.php new file mode 100644 index 0000000..631ce31 --- /dev/null +++ b/assets/php/vendor/react/stream/src/DuplexStreamInterface.php @@ -0,0 +1,39 @@ +<?php + +namespace React\Stream; + +/** + * The `DuplexStreamInterface` is responsible for providing an interface for + * duplex streams (both readable and writable). + * + * It builds on top of the existing interfaces for readable and writable streams + * and follows the exact same method and event semantics. + * If you're new to this concept, you should look into the + * `ReadableStreamInterface` and `WritableStreamInterface` first. + * + * Besides defining a few methods, this interface also implements the + * `EventEmitterInterface` which allows you to react to the same events defined + * on the `ReadbleStreamInterface` and `WritableStreamInterface`. + * + * The event callback functions MUST be a valid `callable` that obeys strict + * parameter definitions and MUST accept event parameters exactly as documented. + * The event callback functions MUST NOT throw an `Exception`. + * The return value of the event callback functions will be ignored and has no + * effect, so for performance reasons you're recommended to not return any + * excessive data structures. + * + * Every implementation of this interface MUST follow these event semantics in + * order to be considered a well-behaving stream. + * + * > Note that higher-level implementations of this interface may choose to + * define additional events with dedicated semantics not defined as part of + * this low-level stream specification. Conformance with these event semantics + * is out of scope for this interface, so you may also have to refer to the + * documentation of such a higher-level implementation. + * + * @see ReadableStreamInterface + * @see WritableStreamInterface + */ +interface DuplexStreamInterface extends ReadableStreamInterface, WritableStreamInterface +{ +} diff --git a/assets/php/vendor/react/stream/src/ReadableResourceStream.php b/assets/php/vendor/react/stream/src/ReadableResourceStream.php new file mode 100644 index 0000000..015a96b --- /dev/null +++ b/assets/php/vendor/react/stream/src/ReadableResourceStream.php @@ -0,0 +1,177 @@ +<?php + +namespace React\Stream; + +use Evenement\EventEmitter; +use React\EventLoop\LoopInterface; +use InvalidArgumentException; + +final class ReadableResourceStream extends EventEmitter implements ReadableStreamInterface +{ + /** + * @var resource + */ + private $stream; + + private $loop; + + /** + * Controls the maximum buffer size in bytes to read at once from the stream. + * + * This value SHOULD NOT be changed unless you know what you're doing. + * + * This can be a positive number which means that up to X bytes will be read + * at once from the underlying stream resource. Note that the actual number + * of bytes read may be lower if the stream resource has less than X bytes + * currently available. + * + * This can be `-1` which means read everything available from the + * underlying stream resource. + * This should read until the stream resource is not readable anymore + * (i.e. underlying buffer drained), note that this does not neccessarily + * mean it reached EOF. + * + * @var int + */ + private $bufferSize; + + private $closed = false; + private $listening = false; + + public function __construct($stream, LoopInterface $loop, $readChunkSize = null) + { + if (!is_resource($stream) || get_resource_type($stream) !== "stream") { + throw new InvalidArgumentException('First parameter must be a valid stream resource'); + } + + // ensure resource is opened for reading (fopen mode must contain "r" or "+") + $meta = stream_get_meta_data($stream); + if (isset($meta['mode']) && $meta['mode'] !== '' && strpos($meta['mode'], 'r') === strpos($meta['mode'], '+')) { + throw new InvalidArgumentException('Given stream resource is not opened in read mode'); + } + + // this class relies on non-blocking I/O in order to not interrupt the event loop + // e.g. pipes on Windows do not support this: https://bugs.php.net/bug.php?id=47918 + if (stream_set_blocking($stream, 0) !== true) { + throw new \RuntimeException('Unable to set stream resource to non-blocking mode'); + } + + // Use unbuffered read operations on the underlying stream resource. + // Reading chunks from the stream may otherwise leave unread bytes in + // PHP's stream buffers which some event loop implementations do not + // trigger events on (edge triggered). + // This does not affect the default event loop implementation (level + // triggered), so we can ignore platforms not supporting this (HHVM). + // Pipe streams (such as STDIN) do not seem to require this and legacy + // PHP versions cause SEGFAULTs on unbuffered pipe streams, so skip this. + if (function_exists('stream_set_read_buffer') && !$this->isLegacyPipe($stream)) { + stream_set_read_buffer($stream, 0); + } + + $this->stream = $stream; + $this->loop = $loop; + $this->bufferSize = ($readChunkSize === null) ? 65536 : (int)$readChunkSize; + + $this->resume(); + } + + public function isReadable() + { + return !$this->closed; + } + + public function pause() + { + if ($this->listening) { + $this->loop->removeReadStream($this->stream); + $this->listening = false; + } + } + + public function resume() + { + if (!$this->listening && !$this->closed) { + $this->loop->addReadStream($this->stream, array($this, 'handleData')); + $this->listening = true; + } + } + + public function pipe(WritableStreamInterface $dest, array $options = array()) + { + return Util::pipe($this, $dest, $options); + } + + public function close() + { + if ($this->closed) { + return; + } + + $this->closed = true; + + $this->emit('close'); + $this->pause(); + $this->removeAllListeners(); + + if (is_resource($this->stream)) { + fclose($this->stream); + } + } + + /** @internal */ + public function handleData() + { + $error = null; + set_error_handler(function ($errno, $errstr, $errfile, $errline) use (&$error) { + $error = new \ErrorException( + $errstr, + 0, + $errno, + $errfile, + $errline + ); + }); + + $data = stream_get_contents($this->stream, $this->bufferSize); + + restore_error_handler(); + + if ($error !== null) { + $this->emit('error', array(new \RuntimeException('Unable to read from stream: ' . $error->getMessage(), 0, $error))); + $this->close(); + return; + } + + if ($data !== '') { + $this->emit('data', array($data)); + } else{ + // no data read => we reached the end and close the stream + $this->emit('end'); + $this->close(); + } + } + + /** + * Returns whether this is a pipe resource in a legacy environment + * + * This works around a legacy PHP bug (#61019) that was fixed in PHP 5.4.28+ + * and PHP 5.5.12+ and newer. + * + * @param resource $resource + * @return bool + * @link https://github.com/reactphp/child-process/issues/40 + * + * @codeCoverageIgnore + */ + private function isLegacyPipe($resource) + { + if (PHP_VERSION_ID < 50428 || (PHP_VERSION_ID >= 50500 && PHP_VERSION_ID < 50512)) { + $meta = stream_get_meta_data($resource); + + if (isset($meta['stream_type']) && $meta['stream_type'] === 'STDIO') { + return true; + } + } + return false; + } +} diff --git a/assets/php/vendor/react/stream/src/ReadableStreamInterface.php b/assets/php/vendor/react/stream/src/ReadableStreamInterface.php new file mode 100644 index 0000000..2b4c3d0 --- /dev/null +++ b/assets/php/vendor/react/stream/src/ReadableStreamInterface.php @@ -0,0 +1,362 @@ +<?php + +namespace React\Stream; + +use Evenement\EventEmitterInterface; + +/** + * The `ReadableStreamInterface` is responsible for providing an interface for + * read-only streams and the readable side of duplex streams. + * + * Besides defining a few methods, this interface also implements the + * `EventEmitterInterface` which allows you to react to certain events: + * + * data event: + * The `data` event will be emitted whenever some data was read/received + * from this source stream. + * The event receives a single mixed argument for incoming data. + * + * ```php + * $stream->on('data', function ($data) { + * echo $data; + * }); + * ``` + * + * This event MAY be emitted any number of times, which may be zero times if + * this stream does not send any data at all. + * It SHOULD not be emitted after an `end` or `close` event. + * + * The given `$data` argument may be of mixed type, but it's usually + * recommended it SHOULD be a `string` value or MAY use a type that allows + * representation as a `string` for maximum compatibility. + * + * Many common streams (such as a TCP/IP connection or a file-based stream) + * will emit the raw (binary) payload data that is received over the wire as + * chunks of `string` values. + * + * Due to the stream-based nature of this, the sender may send any number + * of chunks with varying sizes. There are no guarantees that these chunks + * will be received with the exact same framing the sender intended to send. + * In other words, many lower-level protocols (such as TCP/IP) transfer the + * data in chunks that may be anywhere between single-byte values to several + * dozens of kilobytes. You may want to apply a higher-level protocol to + * these low-level data chunks in order to achieve proper message framing. + * + * end event: + * The `end` event will be emitted once the source stream has successfully + * reached the end of the stream (EOF). + * + * ```php + * $stream->on('end', function () { + * echo 'END'; + * }); + * ``` + * + * This event SHOULD be emitted once or never at all, depending on whether + * a successful end was detected. + * It SHOULD NOT be emitted after a previous `end` or `close` event. + * It MUST NOT be emitted if the stream closes due to a non-successful + * end, such as after a previous `error` event. + * + * After the stream is ended, it MUST switch to non-readable mode, + * see also `isReadable()`. + * + * This event will only be emitted if the *end* was reached successfully, + * not if the stream was interrupted by an unrecoverable error or explicitly + * closed. Not all streams know this concept of a "successful end". + * Many use-cases involve detecting when the stream closes (terminates) + * instead, in this case you should use the `close` event. + * After the stream emits an `end` event, it SHOULD usually be followed by a + * `close` event. + * + * Many common streams (such as a TCP/IP connection or a file-based stream) + * will emit this event if either the remote side closes the connection or + * a file handle was successfully read until reaching its end (EOF). + * + * Note that this event should not be confused with the `end()` method. + * This event defines a successful end *reading* from a source stream, while + * the `end()` method defines *writing* a successful end to a destination + * stream. + * + * error event: + * The `error` event will be emitted once a fatal error occurs, usually while + * trying to read from this stream. + * The event receives a single `Exception` argument for the error instance. + * + * ```php + * $stream->on('error', function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; + * }); + * ``` + * + * This event SHOULD be emitted once the stream detects a fatal error, such + * as a fatal transmission error or after an unexpected `data` or premature + * `end` event. + * It SHOULD NOT be emitted after a previous `error`, `end` or `close` event. + * It MUST NOT be emitted if this is not a fatal error condition, such as + * a temporary network issue that did not cause any data to be lost. + * + * After the stream errors, it MUST close the stream and SHOULD thus be + * followed by a `close` event and then switch to non-readable mode, see + * also `close()` and `isReadable()`. + * + * Many common streams (such as a TCP/IP connection or a file-based stream) + * only deal with data transmission and do not make assumption about data + * boundaries (such as unexpected `data` or premature `end` events). + * In other words, many lower-level protocols (such as TCP/IP) may choose + * to only emit this for a fatal transmission error once and will then + * close (terminate) the stream in response. + * + * If this stream is a `DuplexStreamInterface`, you should also notice + * how the writable side of the stream also implements an `error` event. + * In other words, an error may occur while either reading or writing the + * stream which should result in the same error processing. + * + * close event: + * The `close` event will be emitted once the stream closes (terminates). + * + * ```php + * $stream->on('close', function () { + * echo 'CLOSED'; + * }); + * ``` + * + * This event SHOULD be emitted once or never at all, depending on whether + * the stream ever terminates. + * It SHOULD NOT be emitted after a previous `close` event. + * + * After the stream is closed, it MUST switch to non-readable mode, + * see also `isReadable()`. + * + * Unlike the `end` event, this event SHOULD be emitted whenever the stream + * closes, irrespective of whether this happens implicitly due to an + * unrecoverable error or explicitly when either side closes the stream. + * If you only want to detect a *successful* end, you should use the `end` + * event instead. + * + * Many common streams (such as a TCP/IP connection or a file-based stream) + * will likely choose to emit this event after reading a *successful* `end` + * event or after a fatal transmission `error` event. + * + * If this stream is a `DuplexStreamInterface`, you should also notice + * how the writable side of the stream also implements a `close` event. + * In other words, after receiving this event, the stream MUST switch into + * non-writable AND non-readable mode, see also `isWritable()`. + * Note that this event should not be confused with the `end` event. + * + * The event callback functions MUST be a valid `callable` that obeys strict + * parameter definitions and MUST accept event parameters exactly as documented. + * The event callback functions MUST NOT throw an `Exception`. + * The return value of the event callback functions will be ignored and has no + * effect, so for performance reasons you're recommended to not return any + * excessive data structures. + * + * Every implementation of this interface MUST follow these event semantics in + * order to be considered a well-behaving stream. + * + * > Note that higher-level implementations of this interface may choose to + * define additional events with dedicated semantics not defined as part of + * this low-level stream specification. Conformance with these event semantics + * is out of scope for this interface, so you may also have to refer to the + * documentation of such a higher-level implementation. + * + * @see EventEmitterInterface + */ +interface ReadableStreamInterface extends EventEmitterInterface +{ + /** + * Checks whether this stream is in a readable state (not closed already). + * + * This method can be used to check if the stream still accepts incoming + * data events or if it is ended or closed already. + * Once the stream is non-readable, no further `data` or `end` events SHOULD + * be emitted. + * + * ```php + * assert($stream->isReadable() === false); + * + * $stream->on('data', assertNeverCalled()); + * $stream->on('end', assertNeverCalled()); + * ``` + * + * A successfully opened stream always MUST start in readable mode. + * + * Once the stream ends or closes, it MUST switch to non-readable mode. + * This can happen any time, explicitly through `close()` or + * implicitly due to a remote close or an unrecoverable transmission error. + * Once a stream has switched to non-readable mode, it MUST NOT transition + * back to readable mode. + * + * If this stream is a `DuplexStreamInterface`, you should also notice + * how the writable side of the stream also implements an `isWritable()` + * method. Unless this is a half-open duplex stream, they SHOULD usually + * have the same return value. + * + * @return bool + */ + public function isReadable(); + + /** + * Pauses reading incoming data events. + * + * Removes the data source file descriptor from the event loop. This + * allows you to throttle incoming data. + * + * Unless otherwise noted, a successfully opened stream SHOULD NOT start + * in paused state. + * + * Once the stream is paused, no futher `data` or `end` events SHOULD + * be emitted. + * + * ```php + * $stream->pause(); + * + * $stream->on('data', assertShouldNeverCalled()); + * $stream->on('end', assertShouldNeverCalled()); + * ``` + * + * This method is advisory-only, though generally not recommended, the + * stream MAY continue emitting `data` events. + * + * You can continue processing events by calling `resume()` again. + * + * Note that both methods can be called any number of times, in particular + * calling `pause()` more than once SHOULD NOT have any effect. + * + * @see self::resume() + * @return void + */ + public function pause(); + + /** + * Resumes reading incoming data events. + * + * Re-attach the data source after a previous `pause()`. + * + * ```php + * $stream->pause(); + * + * $loop->addTimer(1.0, function () use ($stream) { + * $stream->resume(); + * }); + * ``` + * + * Note that both methods can be called any number of times, in particular + * calling `resume()` without a prior `pause()` SHOULD NOT have any effect. + * + * @see self::pause() + * @return void + */ + public function resume(); + + /** + * Pipes all the data from this readable source into the given writable destination. + * + * Automatically sends all incoming data to the destination. + * Automatically throttles the source based on what the destination can handle. + * + * ```php + * $source->pipe($dest); + * ``` + * + * Similarly, you can also pipe an instance implementing `DuplexStreamInterface` + * into itself in order to write back all the data that is received. + * This may be a useful feature for a TCP/IP echo service: + * + * ```php + * $connection->pipe($connection); + * ``` + * + * This method returns the destination stream as-is, which can be used to + * set up chains of piped streams: + * + * ```php + * $source->pipe($decodeGzip)->pipe($filterBadWords)->pipe($dest); + * ``` + * + * By default, this will call `end()` on the destination stream once the + * source stream emits an `end` event. This can be disabled like this: + * + * ```php + * $source->pipe($dest, array('end' => false)); + * ``` + * + * Note that this only applies to the `end` event. + * If an `error` or explicit `close` event happens on the source stream, + * you'll have to manually close the destination stream: + * + * ```php + * $source->pipe($dest); + * $source->on('close', function () use ($dest) { + * $dest->end('BYE!'); + * }); + * ``` + * + * If the source stream is not readable (closed state), then this is a NO-OP. + * + * ```php + * $source->close(); + * $source->pipe($dest); // NO-OP + * ``` + * + * If the destinantion stream is not writable (closed state), then this will simply + * throttle (pause) the source stream: + * + * ```php + * $dest->close(); + * $source->pipe($dest); // calls $source->pause() + * ``` + * + * Similarly, if the destination stream is closed while the pipe is still + * active, it will also throttle (pause) the source stream: + * + * ```php + * $source->pipe($dest); + * $dest->close(); // calls $source->pause() + * ``` + * + * Once the pipe is set up successfully, the destination stream MUST emit + * a `pipe` event with this source stream an event argument. + * + * @param WritableStreamInterface $dest + * @param array $options + * @return WritableStreamInterface $dest stream as-is + */ + public function pipe(WritableStreamInterface $dest, array $options = array()); + + /** + * Closes the stream (forcefully). + * + * This method can be used to (forcefully) close the stream. + * + * ```php + * $stream->close(); + * ``` + * + * Once the stream is closed, it SHOULD emit a `close` event. + * Note that this event SHOULD NOT be emitted more than once, in particular + * if this method is called multiple times. + * + * After calling this method, the stream MUST switch into a non-readable + * mode, see also `isReadable()`. + * This means that no further `data` or `end` events SHOULD be emitted. + * + * ```php + * $stream->close(); + * assert($stream->isReadable() === false); + * + * $stream->on('data', assertNeverCalled()); + * $stream->on('end', assertNeverCalled()); + * ``` + * + * If this stream is a `DuplexStreamInterface`, you should also notice + * how the writable side of the stream also implements a `close()` method. + * In other words, after calling this method, the stream MUST switch into + * non-writable AND non-readable mode, see also `isWritable()`. + * Note that this method should not be confused with the `end()` method. + * + * @return void + * @see WritableStreamInterface::close() + */ + public function close(); +} diff --git a/assets/php/vendor/react/stream/src/ThroughStream.php b/assets/php/vendor/react/stream/src/ThroughStream.php new file mode 100644 index 0000000..da2fbb0 --- /dev/null +++ b/assets/php/vendor/react/stream/src/ThroughStream.php @@ -0,0 +1,190 @@ +<?php + +namespace React\Stream; + +use Evenement\EventEmitter; +use InvalidArgumentException; + +/** + * The `ThroughStream` implements the + * [`DuplexStreamInterface`](#duplexstreaminterface) and will simply pass any data + * you write to it through to its readable end. + * + * ```php + * $through = new ThroughStream(); + * $through->on('data', $this->expectCallableOnceWith('hello')); + * + * $through->write('hello'); + * ``` + * + * Similarly, the [`end()` method](#end) will end the stream and emit an + * [`end` event](#end-event) and then [`close()`](#close-1) the stream. + * The [`close()` method](#close-1) will close the stream and emit a + * [`close` event](#close-event). + * Accordingly, this is can also be used in a [`pipe()`](#pipe) context like this: + * + * ```php + * $through = new ThroughStream(); + * $source->pipe($through)->pipe($dest); + * ``` + * + * Optionally, its constructor accepts any callable function which will then be + * used to *filter* any data written to it. This function receives a single data + * argument as passed to the writable side and must return the data as it will be + * passed to its readable end: + * + * ```php + * $through = new ThroughStream('strtoupper'); + * $source->pipe($through)->pipe($dest); + * ``` + * + * Note that this class makes no assumptions about any data types. This can be + * used to convert data, for example for transforming any structured data into + * a newline-delimited JSON (NDJSON) stream like this: + * + * ```php + * $through = new ThroughStream(function ($data) { + * return json_encode($data) . PHP_EOL; + * }); + * $through->on('data', $this->expectCallableOnceWith("[2, true]\n")); + * + * $through->write(array(2, true)); + * ``` + * + * The callback function is allowed to throw an `Exception`. In this case, + * the stream will emit an `error` event and then [`close()`](#close-1) the stream. + * + * ```php + * $through = new ThroughStream(function ($data) { + * if (!is_string($data)) { + * throw new \UnexpectedValueException('Only strings allowed'); + * } + * return $data; + * }); + * $through->on('error', $this->expectCallableOnce())); + * $through->on('close', $this->expectCallableOnce())); + * $through->on('data', $this->expectCallableNever())); + * + * $through->write(2); + * ``` + * + * @see WritableStreamInterface::write() + * @see WritableStreamInterface::end() + * @see DuplexStreamInterface::close() + * @see WritableStreamInterface::pipe() + */ +final class ThroughStream extends EventEmitter implements DuplexStreamInterface +{ + private $readable = true; + private $writable = true; + private $closed = false; + private $paused = false; + private $drain = false; + private $callback; + + public function __construct($callback = null) + { + if ($callback !== null && !is_callable($callback)) { + throw new InvalidArgumentException('Invalid transformation callback given'); + } + + $this->callback = $callback; + } + + public function pause() + { + $this->paused = true; + } + + public function resume() + { + if ($this->drain) { + $this->drain = false; + $this->emit('drain'); + } + $this->paused = false; + } + + public function pipe(WritableStreamInterface $dest, array $options = array()) + { + return Util::pipe($this, $dest, $options); + } + + public function isReadable() + { + return $this->readable; + } + + public function isWritable() + { + return $this->writable; + } + + public function write($data) + { + if (!$this->writable) { + return false; + } + + if ($this->callback !== null) { + try { + $data = call_user_func($this->callback, $data); + } catch (\Exception $e) { + $this->emit('error', array($e)); + $this->close(); + + return false; + } + } + + $this->emit('data', array($data)); + + if ($this->paused) { + $this->drain = true; + return false; + } + + return true; + } + + public function end($data = null) + { + if (!$this->writable) { + return; + } + + if (null !== $data) { + $this->write($data); + + // return if write() already caused the stream to close + if (!$this->writable) { + return; + } + } + + $this->readable = false; + $this->writable = false; + $this->paused = true; + $this->drain = false; + + $this->emit('end'); + $this->close(); + } + + public function close() + { + if ($this->closed) { + return; + } + + $this->readable = false; + $this->writable = false; + $this->closed = true; + $this->paused = true; + $this->drain = false; + $this->callback = null; + + $this->emit('close'); + $this->removeAllListeners(); + } +} diff --git a/assets/php/vendor/react/stream/src/Util.php b/assets/php/vendor/react/stream/src/Util.php new file mode 100644 index 0000000..14ddcfc --- /dev/null +++ b/assets/php/vendor/react/stream/src/Util.php @@ -0,0 +1,75 @@ +<?php + +namespace React\Stream; + +final class Util +{ + /** + * Pipes all the data from the given $source into the $dest + * + * @param ReadableStreamInterface $source + * @param WritableStreamInterface $dest + * @param array $options + * @return WritableStreamInterface $dest stream as-is + * @see ReadableStreamInterface::pipe() for more details + */ + public static function pipe(ReadableStreamInterface $source, WritableStreamInterface $dest, array $options = array()) + { + // source not readable => NO-OP + if (!$source->isReadable()) { + return $dest; + } + + // destination not writable => just pause() source + if (!$dest->isWritable()) { + $source->pause(); + + return $dest; + } + + $dest->emit('pipe', array($source)); + + // forward all source data events as $dest->write() + $source->on('data', $dataer = function ($data) use ($source, $dest) { + $feedMore = $dest->write($data); + + if (false === $feedMore) { + $source->pause(); + } + }); + $dest->on('close', function () use ($source, $dataer) { + $source->removeListener('data', $dataer); + $source->pause(); + }); + + // forward destination drain as $source->resume() + $dest->on('drain', $drainer = function () use ($source) { + $source->resume(); + }); + $source->on('close', function () use ($dest, $drainer) { + $dest->removeListener('drain', $drainer); + }); + + // forward end event from source as $dest->end() + $end = isset($options['end']) ? $options['end'] : true; + if ($end) { + $source->on('end', $ender = function () use ($dest) { + $dest->end(); + }); + $dest->on('close', function () use ($source, $ender) { + $source->removeListener('end', $ender); + }); + } + + return $dest; + } + + public static function forwardEvents($source, $target, array $events) + { + foreach ($events as $event) { + $source->on($event, function () use ($event, $target) { + $target->emit($event, func_get_args()); + }); + } + } +} diff --git a/assets/php/vendor/react/stream/src/WritableResourceStream.php b/assets/php/vendor/react/stream/src/WritableResourceStream.php new file mode 100644 index 0000000..7e04205 --- /dev/null +++ b/assets/php/vendor/react/stream/src/WritableResourceStream.php @@ -0,0 +1,171 @@ +<?php + +namespace React\Stream; + +use Evenement\EventEmitter; +use React\EventLoop\LoopInterface; + +final class WritableResourceStream extends EventEmitter implements WritableStreamInterface +{ + private $stream; + private $loop; + private $softLimit; + private $writeChunkSize; + + private $listening = false; + private $writable = true; + private $closed = false; + private $data = ''; + + public function __construct($stream, LoopInterface $loop, $writeBufferSoftLimit = null, $writeChunkSize = null) + { + if (!is_resource($stream) || get_resource_type($stream) !== "stream") { + throw new \InvalidArgumentException('First parameter must be a valid stream resource'); + } + + // ensure resource is opened for writing (fopen mode must contain either of "waxc+") + $meta = stream_get_meta_data($stream); + if (isset($meta['mode']) && $meta['mode'] !== '' && strtr($meta['mode'], 'waxc+', '.....') === $meta['mode']) { + throw new \InvalidArgumentException('Given stream resource is not opened in write mode'); + } + + // this class relies on non-blocking I/O in order to not interrupt the event loop + // e.g. pipes on Windows do not support this: https://bugs.php.net/bug.php?id=47918 + if (stream_set_blocking($stream, 0) !== true) { + throw new \RuntimeException('Unable to set stream resource to non-blocking mode'); + } + + $this->stream = $stream; + $this->loop = $loop; + $this->softLimit = ($writeBufferSoftLimit === null) ? 65536 : (int)$writeBufferSoftLimit; + $this->writeChunkSize = ($writeChunkSize === null) ? -1 : (int)$writeChunkSize; + } + + public function isWritable() + { + return $this->writable; + } + + public function write($data) + { + if (!$this->writable) { + return false; + } + + $this->data .= $data; + + if (!$this->listening && $this->data !== '') { + $this->listening = true; + + $this->loop->addWriteStream($this->stream, array($this, 'handleWrite')); + } + + return !isset($this->data[$this->softLimit - 1]); + } + + public function end($data = null) + { + if (null !== $data) { + $this->write($data); + } + + $this->writable = false; + + // close immediately if buffer is already empty + // otherwise wait for buffer to flush first + if ($this->data === '') { + $this->close(); + } + } + + public function close() + { + if ($this->closed) { + return; + } + + if ($this->listening) { + $this->listening = false; + $this->loop->removeWriteStream($this->stream); + } + + $this->closed = true; + $this->writable = false; + $this->data = ''; + + $this->emit('close'); + $this->removeAllListeners(); + + if (is_resource($this->stream)) { + fclose($this->stream); + } + } + + /** @internal */ + public function handleWrite() + { + $error = null; + set_error_handler(function ($errno, $errstr, $errfile, $errline) use (&$error) { + $error = array( + 'message' => $errstr, + 'number' => $errno, + 'file' => $errfile, + 'line' => $errline + ); + }); + + if ($this->writeChunkSize === -1) { + $sent = fwrite($this->stream, $this->data); + } else { + $sent = fwrite($this->stream, $this->data, $this->writeChunkSize); + } + + restore_error_handler(); + + // Only report errors if *nothing* could be sent. + // Any hard (permanent) error will fail to send any data at all. + // Sending excessive amounts of data will only flush *some* data and then + // report a temporary error (EAGAIN) which we do not raise here in order + // to keep the stream open for further tries to write. + // Should this turn out to be a permanent error later, it will eventually + // send *nothing* and we can detect this. + if ($sent === 0 || $sent === false) { + if ($error !== null) { + $error = new \ErrorException( + $error['message'], + 0, + $error['number'], + $error['file'], + $error['line'] + ); + } + + $this->emit('error', array(new \RuntimeException('Unable to write to stream: ' . ($error !== null ? $error->getMessage() : 'Unknown error'), 0, $error))); + $this->close(); + + return; + } + + $exceeded = isset($this->data[$this->softLimit - 1]); + $this->data = (string) substr($this->data, $sent); + + // buffer has been above limit and is now below limit + if ($exceeded && !isset($this->data[$this->softLimit - 1])) { + $this->emit('drain'); + } + + // buffer is now completely empty => stop trying to write + if ($this->data === '') { + // stop waiting for resource to be writable + if ($this->listening) { + $this->loop->removeWriteStream($this->stream); + $this->listening = false; + } + + // buffer is end()ing and now completely empty => close buffer + if (!$this->writable) { + $this->close(); + } + } + } +} diff --git a/assets/php/vendor/react/stream/src/WritableStreamInterface.php b/assets/php/vendor/react/stream/src/WritableStreamInterface.php new file mode 100644 index 0000000..3bc932e --- /dev/null +++ b/assets/php/vendor/react/stream/src/WritableStreamInterface.php @@ -0,0 +1,347 @@ +<?php + +namespace React\Stream; + +use Evenement\EventEmitterInterface; + +/** + * The `WritableStreamInterface` is responsible for providing an interface for + * write-only streams and the writable side of duplex streams. + * + * Besides defining a few methods, this interface also implements the + * `EventEmitterInterface` which allows you to react to certain events: + * + * drain event: + * The `drain` event will be emitted whenever the write buffer became full + * previously and is now ready to accept more data. + * + * ```php + * $stream->on('drain', function () use ($stream) { + * echo 'Stream is now ready to accept more data'; + * }); + * ``` + * + * This event SHOULD be emitted once every time the buffer became full + * previously and is now ready to accept more data. + * In other words, this event MAY be emitted any number of times, which may + * be zero times if the buffer never became full in the first place. + * This event SHOULD NOT be emitted if the buffer has not become full + * previously. + * + * This event is mostly used internally, see also `write()` for more details. + * + * pipe event: + * The `pipe` event will be emitted whenever a readable stream is `pipe()`d + * into this stream. + * The event receives a single `ReadableStreamInterface` argument for the + * source stream. + * + * ```php + * $stream->on('pipe', function (ReadableStreamInterface $source) use ($stream) { + * echo 'Now receiving piped data'; + * + * // explicitly close target if source emits an error + * $source->on('error', function () use ($stream) { + * $stream->close(); + * }); + * }); + * + * $source->pipe($stream); + * ``` + * + * This event MUST be emitted once for each readable stream that is + * successfully piped into this destination stream. + * In other words, this event MAY be emitted any number of times, which may + * be zero times if no stream is ever piped into this stream. + * This event MUST NOT be emitted if either the source is not readable + * (closed already) or this destination is not writable (closed already). + * + * This event is mostly used internally, see also `pipe()` for more details. + * + * error event: + * The `error` event will be emitted once a fatal error occurs, usually while + * trying to write to this stream. + * The event receives a single `Exception` argument for the error instance. + * + * ```php + * $stream->on('error', function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; + * }); + * ``` + * + * This event SHOULD be emitted once the stream detects a fatal error, such + * as a fatal transmission error. + * It SHOULD NOT be emitted after a previous `error` or `close` event. + * It MUST NOT be emitted if this is not a fatal error condition, such as + * a temporary network issue that did not cause any data to be lost. + * + * After the stream errors, it MUST close the stream and SHOULD thus be + * followed by a `close` event and then switch to non-writable mode, see + * also `close()` and `isWritable()`. + * + * Many common streams (such as a TCP/IP connection or a file-based stream) + * only deal with data transmission and may choose + * to only emit this for a fatal transmission error once and will then + * close (terminate) the stream in response. + * + * If this stream is a `DuplexStreamInterface`, you should also notice + * how the readable side of the stream also implements an `error` event. + * In other words, an error may occur while either reading or writing the + * stream which should result in the same error processing. + * + * close event: + * The `close` event will be emitted once the stream closes (terminates). + * + * ```php + * $stream->on('close', function () { + * echo 'CLOSED'; + * }); + * ``` + * + * This event SHOULD be emitted once or never at all, depending on whether + * the stream ever terminates. + * It SHOULD NOT be emitted after a previous `close` event. + * + * After the stream is closed, it MUST switch to non-writable mode, + * see also `isWritable()`. + * + * This event SHOULD be emitted whenever the stream closes, irrespective of + * whether this happens implicitly due to an unrecoverable error or + * explicitly when either side closes the stream. + * + * Many common streams (such as a TCP/IP connection or a file-based stream) + * will likely choose to emit this event after flushing the buffer from + * the `end()` method, after receiving a *successful* `end` event or after + * a fatal transmission `error` event. + * + * If this stream is a `DuplexStreamInterface`, you should also notice + * how the readable side of the stream also implements a `close` event. + * In other words, after receiving this event, the stream MUST switch into + * non-writable AND non-readable mode, see also `isReadable()`. + * Note that this event should not be confused with the `end` event. + * + * The event callback functions MUST be a valid `callable` that obeys strict + * parameter definitions and MUST accept event parameters exactly as documented. + * The event callback functions MUST NOT throw an `Exception`. + * The return value of the event callback functions will be ignored and has no + * effect, so for performance reasons you're recommended to not return any + * excessive data structures. + * + * Every implementation of this interface MUST follow these event semantics in + * order to be considered a well-behaving stream. + * + * > Note that higher-level implementations of this interface may choose to + * define additional events with dedicated semantics not defined as part of + * this low-level stream specification. Conformance with these event semantics + * is out of scope for this interface, so you may also have to refer to the + * documentation of such a higher-level implementation. + * + * @see EventEmitterInterface + * @see DuplexStreamInterface + */ +interface WritableStreamInterface extends EventEmitterInterface +{ + /** + * Checks whether this stream is in a writable state (not closed already). + * + * This method can be used to check if the stream still accepts writing + * any data or if it is ended or closed already. + * Writing any data to a non-writable stream is a NO-OP: + * + * ```php + * assert($stream->isWritable() === false); + * + * $stream->write('end'); // NO-OP + * $stream->end('end'); // NO-OP + * ``` + * + * A successfully opened stream always MUST start in writable mode. + * + * Once the stream ends or closes, it MUST switch to non-writable mode. + * This can happen any time, explicitly through `end()` or `close()` or + * implicitly due to a remote close or an unrecoverable transmission error. + * Once a stream has switched to non-writable mode, it MUST NOT transition + * back to writable mode. + * + * If this stream is a `DuplexStreamInterface`, you should also notice + * how the readable side of the stream also implements an `isReadable()` + * method. Unless this is a half-open duplex stream, they SHOULD usually + * have the same return value. + * + * @return bool + */ + public function isWritable(); + + /** + * Write some data into the stream. + * + * A successful write MUST be confirmed with a boolean `true`, which means + * that either the data was written (flushed) immediately or is buffered and + * scheduled for a future write. Note that this interface gives you no + * control over explicitly flushing the buffered data, as finding the + * appropriate time for this is beyond the scope of this interface and left + * up to the implementation of this interface. + * + * Many common streams (such as a TCP/IP connection or file-based stream) + * may choose to buffer all given data and schedule a future flush by using + * an underlying EventLoop to check when the resource is actually writable. + * + * If a stream cannot handle writing (or flushing) the data, it SHOULD emit + * an `error` event and MAY `close()` the stream if it can not recover from + * this error. + * + * If the internal buffer is full after adding `$data`, then `write()` + * SHOULD return `false`, indicating that the caller should stop sending + * data until the buffer drains. + * The stream SHOULD send a `drain` event once the buffer is ready to accept + * more data. + * + * Similarly, if the the stream is not writable (already in a closed state) + * it MUST NOT process the given `$data` and SHOULD return `false`, + * indicating that the caller should stop sending data. + * + * The given `$data` argument MAY be of mixed type, but it's usually + * recommended it SHOULD be a `string` value or MAY use a type that allows + * representation as a `string` for maximum compatibility. + * + * Many common streams (such as a TCP/IP connection or a file-based stream) + * will only accept the raw (binary) payload data that is transferred over + * the wire as chunks of `string` values. + * + * Due to the stream-based nature of this, the sender may send any number + * of chunks with varying sizes. There are no guarantees that these chunks + * will be received with the exact same framing the sender intended to send. + * In other words, many lower-level protocols (such as TCP/IP) transfer the + * data in chunks that may be anywhere between single-byte values to several + * dozens of kilobytes. You may want to apply a higher-level protocol to + * these low-level data chunks in order to achieve proper message framing. + * + * @param mixed|string $data + * @return bool + */ + public function write($data); + + /** + * Successfully ends the stream (after optionally sending some final data). + * + * This method can be used to successfully end the stream, i.e. close + * the stream after sending out all data that is currently buffered. + * + * ```php + * $stream->write('hello'); + * $stream->write('world'); + * $stream->end(); + * ``` + * + * If there's no data currently buffered and nothing to be flushed, then + * this method MAY `close()` the stream immediately. + * + * If there's still data in the buffer that needs to be flushed first, then + * this method SHOULD try to write out this data and only then `close()` + * the stream. + * Once the stream is closed, it SHOULD emit a `close` event. + * + * Note that this interface gives you no control over explicitly flushing + * the buffered data, as finding the appropriate time for this is beyond the + * scope of this interface and left up to the implementation of this + * interface. + * + * Many common streams (such as a TCP/IP connection or file-based stream) + * may choose to buffer all given data and schedule a future flush by using + * an underlying EventLoop to check when the resource is actually writable. + * + * You can optionally pass some final data that is written to the stream + * before ending the stream. If a non-`null` value is given as `$data`, then + * this method will behave just like calling `write($data)` before ending + * with no data. + * + * ```php + * // shorter version + * $stream->end('bye'); + * + * // same as longer version + * $stream->write('bye'); + * $stream->end(); + * ``` + * + * After calling this method, the stream MUST switch into a non-writable + * mode, see also `isWritable()`. + * This means that no further writes are possible, so any additional + * `write()` or `end()` calls have no effect. + * + * ```php + * $stream->end(); + * assert($stream->isWritable() === false); + * + * $stream->write('nope'); // NO-OP + * $stream->end(); // NO-OP + * ``` + * + * If this stream is a `DuplexStreamInterface`, calling this method SHOULD + * also end its readable side, unless the stream supports half-open mode. + * In other words, after calling this method, these streams SHOULD switch + * into non-writable AND non-readable mode, see also `isReadable()`. + * This implies that in this case, the stream SHOULD NOT emit any `data` + * or `end` events anymore. + * Streams MAY choose to use the `pause()` method logic for this, but + * special care may have to be taken to ensure a following call to the + * `resume()` method SHOULD NOT continue emitting readable events. + * + * Note that this method should not be confused with the `close()` method. + * + * @param mixed|string|null $data + * @return void + */ + public function end($data = null); + + /** + * Closes the stream (forcefully). + * + * This method can be used to forcefully close the stream, i.e. close + * the stream without waiting for any buffered data to be flushed. + * If there's still data in the buffer, this data SHOULD be discarded. + * + * ```php + * $stream->close(); + * ``` + * + * Once the stream is closed, it SHOULD emit a `close` event. + * Note that this event SHOULD NOT be emitted more than once, in particular + * if this method is called multiple times. + * + * After calling this method, the stream MUST switch into a non-writable + * mode, see also `isWritable()`. + * This means that no further writes are possible, so any additional + * `write()` or `end()` calls have no effect. + * + * ```php + * $stream->close(); + * assert($stream->isWritable() === false); + * + * $stream->write('nope'); // NO-OP + * $stream->end(); // NO-OP + * ``` + * + * Note that this method should not be confused with the `end()` method. + * Unlike the `end()` method, this method does not take care of any existing + * buffers and simply discards any buffer contents. + * Likewise, this method may also be called after calling `end()` on a + * stream in order to stop waiting for the stream to flush its final data. + * + * ```php + * $stream->end(); + * $loop->addTimer(1.0, function () use ($stream) { + * $stream->close(); + * }); + * ``` + * + * If this stream is a `DuplexStreamInterface`, you should also notice + * how the readable side of the stream also implements a `close()` method. + * In other words, after calling this method, the stream MUST switch into + * non-writable AND non-readable mode, see also `isReadable()`. + * + * @return void + * @see ReadableStreamInterface::close() + */ + public function close(); +} diff --git a/assets/php/vendor/react/stream/tests/CallableStub.php b/assets/php/vendor/react/stream/tests/CallableStub.php new file mode 100644 index 0000000..31cc834 --- /dev/null +++ b/assets/php/vendor/react/stream/tests/CallableStub.php @@ -0,0 +1,10 @@ +<?php + +namespace React\Tests\Stream; + +class CallableStub +{ + public function __invoke() + { + } +} diff --git a/assets/php/vendor/react/stream/tests/CompositeStreamTest.php b/assets/php/vendor/react/stream/tests/CompositeStreamTest.php new file mode 100644 index 0000000..df89c3e --- /dev/null +++ b/assets/php/vendor/react/stream/tests/CompositeStreamTest.php @@ -0,0 +1,267 @@ +<?php + +namespace React\Tests\Stream; + +use React\Stream\CompositeStream; +use React\Stream\ThroughStream; + +/** + * @covers React\Stream\CompositeStream + */ +class CompositeStreamTest extends TestCase +{ + /** @test */ + public function itShouldCloseReadableIfNotWritable() + { + $readable = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $readable + ->expects($this->once()) + ->method('isReadable') + ->willReturn(true); + $readable + ->expects($this->once()) + ->method('close'); + + $writable = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $writable + ->expects($this->once()) + ->method('isWritable') + ->willReturn(false); + + $composite = new CompositeStream($readable, $writable); + + $composite->on('close', $this->expectCallableNever()); + $composite->close(); + } + + /** @test */ + public function itShouldCloseWritableIfNotReadable() + { + $readable = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $readable + ->expects($this->once()) + ->method('isReadable') + ->willReturn(false); + + $writable = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $writable + ->expects($this->once()) + ->method('close'); + + $composite = new CompositeStream($readable, $writable); + + $composite->on('close', $this->expectCallableNever()); + $composite->close(); + } + + /** @test */ + public function itShouldForwardWritableCallsToWritableStream() + { + $readable = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $readable + ->expects($this->once()) + ->method('isReadable') + ->willReturn(true); + + $writable = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $writable + ->expects($this->once()) + ->method('write') + ->with('foo'); + $writable + ->expects($this->exactly(2)) + ->method('isWritable') + ->willReturn(true); + + $composite = new CompositeStream($readable, $writable); + $composite->write('foo'); + $composite->isWritable(); + } + + /** @test */ + public function itShouldForwardReadableCallsToReadableStream() + { + $readable = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $readable + ->expects($this->exactly(2)) + ->method('isReadable') + ->willReturn(true); + $readable + ->expects($this->once()) + ->method('pause'); + $readable + ->expects($this->once()) + ->method('resume'); + + $writable = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $writable + ->expects($this->any()) + ->method('isWritable') + ->willReturn(true); + + $composite = new CompositeStream($readable, $writable); + $composite->isReadable(); + $composite->pause(); + $composite->resume(); + } + + /** @test */ + public function itShouldNotForwardResumeIfStreamIsNotWritable() + { + $readable = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $readable + ->expects($this->once()) + ->method('isReadable') + ->willReturn(true); + $readable + ->expects($this->never()) + ->method('resume'); + + $writable = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $writable + ->expects($this->exactly(2)) + ->method('isWritable') + ->willReturnOnConsecutiveCalls(true, false); + + $composite = new CompositeStream($readable, $writable); + $composite->resume(); + } + + /** @test */ + public function endShouldDelegateToWritableWithData() + { + $readable = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $readable + ->expects($this->once()) + ->method('isReadable') + ->willReturn(true); + + $writable = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $writable + ->expects($this->once()) + ->method('isWritable') + ->willReturn(true); + $writable + ->expects($this->once()) + ->method('end') + ->with('foo'); + + $composite = new CompositeStream($readable, $writable); + $composite->end('foo'); + } + + /** @test */ + public function closeShouldCloseBothStreams() + { + $readable = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $readable + ->expects($this->once()) + ->method('isReadable') + ->willReturn(true); + $readable + ->expects($this->once()) + ->method('close'); + + $writable = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $writable + ->expects($this->once()) + ->method('isWritable') + ->willReturn(true); + $writable + ->expects($this->once()) + ->method('close'); + + $composite = new CompositeStream($readable, $writable); + $composite->close(); + } + + /** @test */ + public function itShouldForwardCloseOnlyOnce() + { + $readable = new ThroughStream(); + $writable = new ThroughStream(); + + $composite = new CompositeStream($readable, $writable); + $composite->on('close', $this->expectCallableOnce()); + + $readable->close(); + $writable->close(); + } + + /** @test */ + public function itShouldForwardCloseAndRemoveAllListeners() + { + $in = new ThroughStream(); + + $composite = new CompositeStream($in, $in); + $composite->on('close', $this->expectCallableOnce()); + + $this->assertTrue($composite->isReadable()); + $this->assertTrue($composite->isWritable()); + $this->assertCount(1, $composite->listeners('close')); + + $composite->close(); + + $this->assertFalse($composite->isReadable()); + $this->assertFalse($composite->isWritable()); + $this->assertCount(0, $composite->listeners('close')); + } + + /** @test */ + public function itShouldReceiveForwardedEvents() + { + $readable = new ThroughStream(); + $writable = new ThroughStream(); + + $composite = new CompositeStream($readable, $writable); + $composite->on('data', $this->expectCallableOnce()); + $composite->on('drain', $this->expectCallableOnce()); + + $readable->emit('data', array('foo')); + $writable->emit('drain'); + } + + /** @test */ + public function itShouldHandlePipingCorrectly() + { + $readable = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $readable + ->expects($this->once()) + ->method('isReadable') + ->willReturn(true); + + $writable = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $writable->expects($this->any())->method('isWritable')->willReturn(True); + $writable + ->expects($this->once()) + ->method('write') + ->with('foo'); + + $composite = new CompositeStream($readable, $writable); + + $input = new ThroughStream(); + $input->pipe($composite); + $input->emit('data', array('foo')); + } + + /** @test */ + public function itShouldForwardPipeCallsToReadableStream() + { + $readable = new ThroughStream(); + + $writable = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $writable->expects($this->any())->method('isWritable')->willReturn(True); + + $composite = new CompositeStream($readable, $writable); + + $output = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $output->expects($this->any())->method('isWritable')->willReturn(True); + $output + ->expects($this->once()) + ->method('write') + ->with('foo'); + + $composite->pipe($output); + $readable->emit('data', array('foo')); + } +} diff --git a/assets/php/vendor/react/stream/tests/DuplexResourceStreamIntegrationTest.php b/assets/php/vendor/react/stream/tests/DuplexResourceStreamIntegrationTest.php new file mode 100644 index 0000000..fb5f02a --- /dev/null +++ b/assets/php/vendor/react/stream/tests/DuplexResourceStreamIntegrationTest.php @@ -0,0 +1,352 @@ +<?php + +namespace React\Tests\Stream; + +use React\Stream\DuplexResourceStream; +use React\Stream\ReadableResourceStream; +use React\EventLoop\ExtEventLoop; +use React\EventLoop\ExtLibeventLoop; +use React\EventLoop\ExtLibevLoop; +use React\EventLoop\LoopInterface; +use React\EventLoop\LibEventLoop; +use React\EventLoop\LibEvLoop; +use React\EventLoop\StreamSelectLoop; + +class DuplexResourceStreamIntegrationTest extends TestCase +{ + public function loopProvider() + { + return array( + array( + function() { + return true; + }, + function () { + return new StreamSelectLoop(); + } + ), + array( + function () { + return function_exists('event_base_new'); + }, + function () { + return class_exists('React\EventLoop\ExtLibeventLoop') ? new ExtLibeventLoop() : new LibEventLoop(); + } + ), + array( + function () { + return class_exists('libev\EventLoop'); + }, + function () { + return class_exists('React\EventLoop\ExtLibevLoop') ? new ExtLibevLoop() : new LibEvLoop(); + } + ), + array( + function () { + return class_exists('EventBase') && class_exists('React\EventLoop\ExtEventLoop'); + }, + function () { + return new ExtEventLoop(); + } + ) + ); + } + + /** + * @dataProvider loopProvider + */ + public function testBufferReadsLargeChunks($condition, $loopFactory) + { + if (true !== $condition()) { + return $this->markTestSkipped('Loop implementation not available'); + } + + $loop = $loopFactory(); + + list($sockA, $sockB) = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0); + + $bufferSize = 4096; + $streamA = new DuplexResourceStream($sockA, $loop, $bufferSize); + $streamB = new DuplexResourceStream($sockB, $loop, $bufferSize); + + $testString = str_repeat("*", $bufferSize + 1); + + $buffer = ""; + $streamB->on('data', function ($data) use (&$buffer) { + $buffer .= $data; + }); + + $streamA->write($testString); + + $this->loopTick($loop); + $this->loopTick($loop); + $this->loopTick($loop); + + $streamA->close(); + $streamB->close(); + + $this->assertEquals($testString, $buffer); + } + + /** + * @dataProvider loopProvider + */ + public function testWriteLargeChunk($condition, $loopFactory) + { + if (true !== $condition()) { + return $this->markTestSkipped('Loop implementation not available'); + } + + $loop = $loopFactory(); + + list($sockA, $sockB) = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0); + + $streamA = new DuplexResourceStream($sockA, $loop); + $streamB = new DuplexResourceStream($sockB, $loop); + + // limit seems to be 192 KiB + $size = 256 * 1024; + + // sending side sends and expects clean close with no errors + $streamA->end(str_repeat('*', $size)); + $streamA->on('close', $this->expectCallableOnce()); + $streamA->on('error', $this->expectCallableNever()); + + // receiving side counts bytes and expects clean close with no errors + $received = 0; + $streamB->on('data', function ($chunk) use (&$received) { + $received += strlen($chunk); + }); + $streamB->on('close', $this->expectCallableOnce()); + $streamB->on('error', $this->expectCallableNever()); + + $loop->run(); + + $streamA->close(); + $streamB->close(); + + $this->assertEquals($size, $received); + } + + /** + * @dataProvider loopProvider + */ + public function testDoesNotEmitDataIfNothingHasBeenWritten($condition, $loopFactory) + { + if (true !== $condition()) { + return $this->markTestSkipped('Loop implementation not available'); + } + + $loop = $loopFactory(); + + list($sockA, $sockB) = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0); + + $streamA = new DuplexResourceStream($sockA, $loop); + $streamB = new DuplexResourceStream($sockB, $loop); + + // end streamA without writing any data + $streamA->end(); + + // streamB should not emit any data + $streamB->on('data', $this->expectCallableNever()); + + $loop->run(); + + $streamA->close(); + $streamB->close(); + } + + /** + * @dataProvider loopProvider + */ + public function testDoesNotWriteDataIfRemoteSideFromPairHasBeenClosed($condition, $loopFactory) + { + if (true !== $condition()) { + return $this->markTestSkipped('Loop implementation not available'); + } + + $loop = $loopFactory(); + + list($sockA, $sockB) = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0); + + $streamA = new DuplexResourceStream($sockA, $loop); + $streamB = new DuplexResourceStream($sockB, $loop); + + // end streamA without writing any data + $streamA->pause(); + $streamA->write('hello'); + $streamA->on('close', $this->expectCallableOnce()); + + $streamB->on('data', $this->expectCallableNever()); + $streamB->close(); + + $loop->run(); + + $streamA->close(); + $streamB->close(); + } + + /** + * @dataProvider loopProvider + */ + public function testDoesNotWriteDataIfServerSideHasBeenClosed($condition, $loopFactory) + { + if (true !== $condition()) { + return $this->markTestSkipped('Loop implementation not available'); + } + + $loop = $loopFactory(); + + $server = stream_socket_server('tcp://127.0.0.1:0'); + + $client = stream_socket_client(stream_socket_get_name($server, false)); + $peer = stream_socket_accept($server); + + $streamA = new DuplexResourceStream($client, $loop); + $streamB = new DuplexResourceStream($peer, $loop); + + // end streamA without writing any data + $streamA->pause(); + $streamA->write('hello'); + $streamA->on('close', $this->expectCallableOnce()); + + $streamB->on('data', $this->expectCallableNever()); + $streamB->close(); + + $loop->run(); + + $streamA->close(); + $streamB->close(); + } + + /** + * @dataProvider loopProvider + */ + public function testDoesNotWriteDataIfClientSideHasBeenClosed($condition, $loopFactory) + { + if (true !== $condition()) { + return $this->markTestSkipped('Loop implementation not available'); + } + + $loop = $loopFactory(); + + $server = stream_socket_server('tcp://127.0.0.1:0'); + + $client = stream_socket_client(stream_socket_get_name($server, false)); + $peer = stream_socket_accept($server); + + $streamA = new DuplexResourceStream($peer, $loop); + $streamB = new DuplexResourceStream($client, $loop); + + // end streamA without writing any data + $streamA->pause(); + $streamA->write('hello'); + $streamA->on('close', $this->expectCallableOnce()); + + $streamB->on('data', $this->expectCallableNever()); + $streamB->close(); + + $loop->run(); + + $streamA->close(); + $streamB->close(); + } + + /** + * @dataProvider loopProvider + */ + public function testReadsSingleChunkFromProcessPipe($condition, $loopFactory) + { + if (true !== $condition()) { + return $this->markTestSkipped('Loop implementation not available'); + } + + $loop = $loopFactory(); + + $stream = new ReadableResourceStream(popen('echo test', 'r'), $loop); + $stream->on('data', $this->expectCallableOnceWith("test\n")); + $stream->on('end', $this->expectCallableOnce()); + $stream->on('error', $this->expectCallableNever()); + + $loop->run(); + } + + /** + * @dataProvider loopProvider + */ + public function testReadsMultipleChunksFromProcessPipe($condition, $loopFactory) + { + if (true !== $condition()) { + return $this->markTestSkipped('Loop implementation not available'); + } + + $loop = $loopFactory(); + + $stream = new ReadableResourceStream(popen('echo a;sleep 0.1;echo b;sleep 0.1;echo c', 'r'), $loop); + + $buffer = ''; + $stream->on('data', function ($chunk) use (&$buffer) { + $buffer .= $chunk; + }); + + $stream->on('end', $this->expectCallableOnce()); + $stream->on('error', $this->expectCallableNever()); + + $loop->run(); + + $this->assertEquals("a\n" . "b\n" . "c\n", $buffer); + } + + /** + * @dataProvider loopProvider + */ + public function testReadsLongChunksFromProcessPipe($condition, $loopFactory) + { + if (true !== $condition()) { + return $this->markTestSkipped('Loop implementation not available'); + } + + $loop = $loopFactory(); + + $stream = new ReadableResourceStream(popen('dd if=/dev/zero bs=12345 count=1234 2>&-', 'r'), $loop); + + $bytes = 0; + $stream->on('data', function ($chunk) use (&$bytes) { + $bytes += strlen($chunk); + }); + + $stream->on('end', $this->expectCallableOnce()); + $stream->on('error', $this->expectCallableNever()); + + $loop->run(); + + $this->assertEquals(12345 * 1234, $bytes); + } + + /** + * @dataProvider loopProvider + */ + public function testReadsNothingFromProcessPipeWithNoOutput($condition, $loopFactory) + { + if (true !== $condition()) { + return $this->markTestSkipped('Loop implementation not available'); + } + + $loop = $loopFactory(); + + $stream = new ReadableResourceStream(popen('true', 'r'), $loop); + $stream->on('data', $this->expectCallableNever()); + $stream->on('end', $this->expectCallableOnce()); + $stream->on('error', $this->expectCallableNever()); + + $loop->run(); + } + + private function loopTick(LoopInterface $loop) + { + $loop->addTimer(0, function () use ($loop) { + $loop->stop(); + }); + $loop->run(); + } +} diff --git a/assets/php/vendor/react/stream/tests/DuplexResourceStreamTest.php b/assets/php/vendor/react/stream/tests/DuplexResourceStreamTest.php new file mode 100644 index 0000000..3212ae8 --- /dev/null +++ b/assets/php/vendor/react/stream/tests/DuplexResourceStreamTest.php @@ -0,0 +1,495 @@ +<?php + +namespace React\Tests\Stream; + +use React\Stream\DuplexResourceStream; +use Clue\StreamFilter as Filter; +use React\Stream\WritableResourceStream; + +class DuplexResourceStreamTest extends TestCase +{ + /** + * @covers React\Stream\DuplexResourceStream::__construct + * @doesNotPerformAssertions + */ + public function testConstructor() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createLoopMock(); + + new DuplexResourceStream($stream, $loop); + } + + /** + * @covers React\Stream\DuplexResourceStream::__construct + * @doesNotPerformAssertions + */ + public function testConstructorWithExcessiveMode() + { + // excessive flags are ignored for temp streams, so we have to use a file stream + $name = tempnam(sys_get_temp_dir(), 'test'); + $stream = @fopen($name, 'r+eANYTHING'); + unlink($name); + + $loop = $this->createLoopMock(); + $buffer = new DuplexResourceStream($stream, $loop); + $buffer->close(); + } + + /** + * @covers React\Stream\DuplexResourceStream::__construct + * @expectedException InvalidArgumentException + */ + public function testConstructorThrowsExceptionOnInvalidStream() + { + $loop = $this->createLoopMock(); + + new DuplexResourceStream('breakme', $loop); + } + + /** + * @covers React\Stream\DuplexResourceStream::__construct + * @expectedException InvalidArgumentException + */ + public function testConstructorThrowsExceptionOnWriteOnlyStream() + { + if (defined('HHVM_VERSION')) { + $this->markTestSkipped('HHVM does not report fopen mode for STDOUT'); + } + + $loop = $this->createLoopMock(); + + new DuplexResourceStream(STDOUT, $loop); + } + + /** + * @covers React\Stream\DuplexResourceStream::__construct + * @expectedException InvalidArgumentException + */ + public function testConstructorThrowsExceptionOnWriteOnlyStreamWithExcessiveMode() + { + // excessive flags are ignored for temp streams, so we have to use a file stream + $name = tempnam(sys_get_temp_dir(), 'test'); + $stream = fopen($name, 'weANYTHING'); + unlink($name); + + $loop = $this->createLoopMock(); + new DuplexResourceStream($stream, $loop); + } + + /** + * @covers React\Stream\DuplexResourceStream::__construct + * @expectedException RunTimeException + */ + public function testConstructorThrowsExceptionIfStreamDoesNotSupportNonBlocking() + { + if (!in_array('blocking', stream_get_wrappers())) { + stream_wrapper_register('blocking', 'React\Tests\Stream\EnforceBlockingWrapper'); + } + + $stream = fopen('blocking://test', 'r+'); + $loop = $this->createLoopMock(); + + new DuplexResourceStream($stream, $loop); + } + + /** + * @covers React\Stream\DuplexResourceStream::__construct + * @doesNotPerformAssertions + */ + public function testConstructorAcceptsBuffer() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createLoopMock(); + + $buffer = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + + $conn = new DuplexResourceStream($stream, $loop, null, $buffer); + } + + public function testCloseShouldEmitCloseEvent() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createLoopMock(); + + $conn = new DuplexResourceStream($stream, $loop); + $conn->on('close', $this->expectCallableOnce()); + $conn->on('end', $this->expectCallableNever()); + + $conn->close(); + + $this->assertFalse($conn->isReadable()); + } + + public function testEndShouldEndBuffer() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createLoopMock(); + + $buffer = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $buffer->expects($this->once())->method('end')->with('foo'); + + $conn = new DuplexResourceStream($stream, $loop, null, $buffer); + $conn->end('foo'); + } + + + public function testEndAfterCloseIsNoOp() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createLoopMock(); + + $buffer = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $buffer->expects($this->never())->method('end'); + + $conn = new DuplexResourceStream($stream, $loop); + $conn->close(); + $conn->end(); + } + + /** + * @covers React\Stream\DuplexResourceStream::__construct + * @covers React\Stream\DuplexResourceStream::handleData + */ + public function testDataEvent() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createLoopMock(); + + $capturedData = null; + + $conn = new DuplexResourceStream($stream, $loop); + $conn->on('data', function ($data) use (&$capturedData) { + $capturedData = $data; + }); + + fwrite($stream, "foobar\n"); + rewind($stream); + + $conn->handleData($stream); + $this->assertSame("foobar\n", $capturedData); + } + + /** + * @covers React\Stream\DuplexResourceStream::__construct + * @covers React\Stream\DuplexResourceStream::handleData + */ + public function testDataEventDoesEmitOneChunkMatchingBufferSize() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createLoopMock(); + + $capturedData = null; + + $conn = new DuplexResourceStream($stream, $loop, 4321); + $conn->on('data', function ($data) use (&$capturedData) { + $capturedData = $data; + }); + + fwrite($stream, str_repeat("a", 100000)); + rewind($stream); + + $conn->handleData($stream); + + $this->assertTrue($conn->isReadable()); + $this->assertEquals(4321, strlen($capturedData)); + } + + /** + * @covers React\Stream\DuplexResourceStream::__construct + * @covers React\Stream\DuplexResourceStream::handleData + */ + public function testDataEventDoesEmitOneChunkUntilStreamEndsWhenBufferSizeIsInfinite() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createLoopMock(); + + $capturedData = null; + + $conn = new DuplexResourceStream($stream, $loop, -1); + + $conn->on('data', function ($data) use (&$capturedData) { + $capturedData = $data; + }); + + fwrite($stream, str_repeat("a", 100000)); + rewind($stream); + + $conn->handleData($stream); + + $this->assertTrue($conn->isReadable()); + $this->assertEquals(100000, strlen($capturedData)); + } + + /** + * @covers React\Stream\DuplexResourceStream::handleData + */ + public function testEmptyStreamShouldNotEmitData() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createLoopMock(); + + $conn = new DuplexResourceStream($stream, $loop); + $conn->on('data', $this->expectCallableNever()); + + $conn->handleData($stream); + } + + /** + * @covers React\Stream\DuplexResourceStream::write + */ + public function testWrite() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createWriteableLoopMock(); + + $conn = new DuplexResourceStream($stream, $loop); + $conn->write("foo\n"); + + rewind($stream); + $this->assertSame("foo\n", fgets($stream)); + } + + /** + * @covers React\Stream\DuplexResourceStream::end + * @covers React\Stream\DuplexResourceStream::isReadable + * @covers React\Stream\DuplexResourceStream::isWritable + */ + public function testEnd() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createLoopMock(); + + $conn = new DuplexResourceStream($stream, $loop); + $conn->end(); + + $this->assertFalse(is_resource($stream)); + $this->assertFalse($conn->isReadable()); + $this->assertFalse($conn->isWritable()); + } + + /** + * @covers React\Stream\DuplexResourceStream::end + */ + public function testEndRemovesReadStreamFromLoop() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createLoopMock(); + $loop->expects($this->once())->method('addReadStream')->with($stream); + $loop->expects($this->once())->method('removeReadStream')->with($stream); + + $conn = new DuplexResourceStream($stream, $loop); + $conn->end('bye'); + } + + /** + * @covers React\Stream\DuplexResourceStream::pause + */ + public function testPauseRemovesReadStreamFromLoop() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createLoopMock(); + $loop->expects($this->once())->method('addReadStream')->with($stream); + $loop->expects($this->once())->method('removeReadStream')->with($stream); + + $conn = new DuplexResourceStream($stream, $loop); + $conn->pause(); + $conn->pause(); + } + + /** + * @covers React\Stream\DuplexResourceStream::pause + */ + public function testResumeDoesAddStreamToLoopOnlyOnce() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createLoopMock(); + $loop->expects($this->once())->method('addReadStream')->with($stream); + + $conn = new DuplexResourceStream($stream, $loop); + $conn->resume(); + $conn->resume(); + } + + /** + * @covers React\Stream\DuplexResourceStream::close + */ + public function testCloseRemovesReadStreamFromLoop() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createLoopMock(); + $loop->expects($this->once())->method('addReadStream')->with($stream); + $loop->expects($this->once())->method('removeReadStream')->with($stream); + + $conn = new DuplexResourceStream($stream, $loop); + $conn->close(); + } + + /** + * @covers React\Stream\DuplexResourceStream::close + */ + public function testCloseAfterPauseRemovesReadStreamFromLoopOnlyOnce() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createLoopMock(); + $loop->expects($this->once())->method('addReadStream')->with($stream); + $loop->expects($this->once())->method('removeReadStream')->with($stream); + + $conn = new DuplexResourceStream($stream, $loop); + $conn->pause(); + $conn->close(); + } + + /** + * @covers React\Stream\DuplexResourceStream::close + */ + public function testResumeAfterCloseDoesAddReadStreamToLoopOnlyOnce() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createLoopMock(); + $loop->expects($this->once())->method('addReadStream')->with($stream); + + $conn = new DuplexResourceStream($stream, $loop); + $conn->close(); + $conn->resume(); + } + + public function testEndedStreamsShouldNotWrite() + { + $file = tempnam(sys_get_temp_dir(), 'reactphptest_'); + $stream = fopen($file, 'r+'); + $loop = $this->createWriteableLoopMock(); + + $conn = new DuplexResourceStream($stream, $loop); + $conn->write("foo\n"); + $conn->end(); + + $res = $conn->write("bar\n"); + $stream = fopen($file, 'r'); + + $this->assertSame("foo\n", fgets($stream)); + $this->assertFalse($res); + + unlink($file); + } + + public function testPipeShouldReturnDestination() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createLoopMock(); + + $conn = new DuplexResourceStream($stream, $loop); + $dest = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + + $this->assertSame($dest, $conn->pipe($dest)); + } + + public function testBufferEventsShouldBubbleUp() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createLoopMock(); + + $buffer = new WritableResourceStream($stream, $loop); + $conn = new DuplexResourceStream($stream, $loop, null, $buffer); + + $conn->on('drain', $this->expectCallableOnce()); + $conn->on('error', $this->expectCallableOnce()); + + $buffer->emit('drain'); + $buffer->emit('error', array(new \RuntimeException('Whoops'))); + } + + /** + * @covers React\Stream\DuplexResourceStream::handleData + */ + public function testClosingStreamInDataEventShouldNotTriggerError() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createLoopMock(); + + $conn = new DuplexResourceStream($stream, $loop); + $conn->on('error', $this->expectCallableNever()); + $conn->on('data', function ($data) use ($conn) { + $conn->close(); + }); + + fwrite($stream, "foobar\n"); + rewind($stream); + + $conn->handleData($stream); + } + + /** + * @covers React\Stream\DuplexResourceStream::handleData + */ + public function testDataFiltered() + { + $stream = fopen('php://temp', 'r+'); + + // add a filter which removes every 'a' when reading + Filter\append($stream, function ($chunk) { + return str_replace('a', '', $chunk); + }, STREAM_FILTER_READ); + + $loop = $this->createLoopMock(); + + $capturedData = null; + + $conn = new DuplexResourceStream($stream, $loop); + $conn->on('data', function ($data) use (&$capturedData) { + $capturedData = $data; + }); + + fwrite($stream, "foobar\n"); + rewind($stream); + + $conn->handleData($stream); + $this->assertSame("foobr\n", $capturedData); + } + + /** + * @covers React\Stream\DuplexResourceStream::handleData + */ + public function testDataErrorShouldEmitErrorAndClose() + { + $stream = fopen('php://temp', 'r+'); + + // add a filter which returns an error when encountering an 'a' when reading + Filter\append($stream, function ($chunk) { + if (strpos($chunk, 'a') !== false) { + throw new \Exception('Invalid'); + } + return $chunk; + }, STREAM_FILTER_READ); + + $loop = $this->createLoopMock(); + + $conn = new DuplexResourceStream($stream, $loop); + $conn->on('data', $this->expectCallableNever()); + $conn->on('error', $this->expectCallableOnce()); + $conn->on('close', $this->expectCallableOnce()); + + fwrite($stream, "foobar\n"); + rewind($stream); + + $conn->handleData($stream); + } + + private function createWriteableLoopMock() + { + $loop = $this->createLoopMock(); + $loop + ->expects($this->once()) + ->method('addWriteStream') + ->will($this->returnCallback(function ($stream, $listener) { + call_user_func($listener, $stream); + })); + + return $loop; + } + + private function createLoopMock() + { + return $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + } +} diff --git a/assets/php/vendor/react/stream/tests/EnforceBlockingWrapper.php b/assets/php/vendor/react/stream/tests/EnforceBlockingWrapper.php new file mode 100644 index 0000000..39c0487 --- /dev/null +++ b/assets/php/vendor/react/stream/tests/EnforceBlockingWrapper.php @@ -0,0 +1,35 @@ +<?php + +namespace React\Tests\Stream; + +/** + * Used to test dummy stream resources that do not support setting non-blocking mode + * + * @link http://php.net/manual/de/class.streamwrapper.php + */ +class EnforceBlockingWrapper +{ + public function stream_open($path, $mode, $options, &$opened_path) + { + return true; + } + + public function stream_cast($cast_as) + { + return false; + } + + public function stream_eof() + { + return false; + } + + public function stream_set_option($option, $arg1, $arg2) + { + if ($option === STREAM_OPTION_BLOCKING) { + return false; + } + + return true; + } +} diff --git a/assets/php/vendor/react/stream/tests/FunctionalInternetTest.php b/assets/php/vendor/react/stream/tests/FunctionalInternetTest.php new file mode 100644 index 0000000..4d31e8e --- /dev/null +++ b/assets/php/vendor/react/stream/tests/FunctionalInternetTest.php @@ -0,0 +1,122 @@ +<?php + +namespace React\Tests\Stream; + +use React\EventLoop\Factory; +use React\EventLoop\LoopInterface; +use React\Stream\DuplexResourceStream; +use React\Stream\WritableResourceStream; + +/** + * @group internet + */ +class FunctionalInternetTest extends TestCase +{ + public function testUploadKilobytePlain() + { + $size = 1000; + $stream = stream_socket_client('tcp://httpbin.org:80'); + + $loop = Factory::create(); + $stream = new DuplexResourceStream($stream, $loop); + + $buffer = ''; + $stream->on('data', function ($chunk) use (&$buffer) { + $buffer .= $chunk; + }); + + $stream->on('error', $this->expectCallableNever()); + + $stream->write("POST /post HTTP/1.0\r\nHost: httpbin.org\r\nContent-Length: $size\r\n\r\n" . str_repeat('.', $size)); + + $this->awaitStreamClose($stream, $loop); + + $this->assertNotEquals('', $buffer); + } + + public function testUploadBiggerBlockPlain() + { + $size = 50 * 1000; + $stream = stream_socket_client('tcp://httpbin.org:80'); + + $loop = Factory::create(); + $stream = new DuplexResourceStream($stream, $loop); + + $buffer = ''; + $stream->on('data', function ($chunk) use (&$buffer) { + $buffer .= $chunk; + }); + + $stream->on('error', $this->expectCallableNever()); + + $stream->write("POST /post HTTP/1.0\r\nHost: httpbin.org\r\nContent-Length: $size\r\n\r\n" . str_repeat('.', $size)); + + $this->awaitStreamClose($stream, $loop); + + $this->assertNotEquals('', $buffer); + } + + public function testUploadKilobyteSecure() + { + $size = 1000; + $stream = stream_socket_client('tls://httpbin.org:443'); + + $loop = Factory::create(); + $stream = new DuplexResourceStream($stream, $loop); + + $buffer = ''; + $stream->on('data', function ($chunk) use (&$buffer) { + $buffer .= $chunk; + }); + + $stream->on('error', $this->expectCallableNever()); + + $stream->write("POST /post HTTP/1.0\r\nHost: httpbin.org\r\nContent-Length: $size\r\n\r\n" . str_repeat('.', $size)); + + $this->awaitStreamClose($stream, $loop); + + $this->assertNotEquals('', $buffer); + } + + public function testUploadBiggerBlockSecureRequiresSmallerChunkSize() + { + $size = 50 * 1000; + $stream = stream_socket_client('tls://httpbin.org:443'); + + $loop = Factory::create(); + $stream = new DuplexResourceStream( + $stream, + $loop, + null, + new WritableResourceStream($stream, $loop, null, 8192) + ); + + $buffer = ''; + $stream->on('data', function ($chunk) use (&$buffer) { + $buffer .= $chunk; + }); + + $stream->on('error', $this->expectCallableNever()); + + $stream->write("POST /post HTTP/1.0\r\nHost: httpbin.org\r\nContent-Length: $size\r\n\r\n" . str_repeat('.', $size)); + + $this->awaitStreamClose($stream, $loop); + + $this->assertNotEquals('', $buffer); + } + + private function awaitStreamClose(DuplexResourceStream $stream, LoopInterface $loop, $timeout = 10.0) + { + $stream->on('close', function () use ($loop) { + $loop->stop(); + }); + + $that = $this; + $loop->addTimer($timeout, function () use ($loop, $that) { + $loop->stop(); + $that->fail('Timed out while waiting for stream to close'); + }); + + $loop->run(); + } +} diff --git a/assets/php/vendor/react/stream/tests/ReadableResourceStreamTest.php b/assets/php/vendor/react/stream/tests/ReadableResourceStreamTest.php new file mode 100644 index 0000000..20da96f --- /dev/null +++ b/assets/php/vendor/react/stream/tests/ReadableResourceStreamTest.php @@ -0,0 +1,372 @@ +<?php + +namespace React\Tests\Stream; + +use React\Stream\ReadableResourceStream; +use Clue\StreamFilter as Filter; + +class ReadableResourceStreamTest extends TestCase +{ + /** + * @covers React\Stream\ReadableResourceStream::__construct + * @doesNotPerformAssertions + */ + public function testConstructor() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createLoopMock(); + + new ReadableResourceStream($stream, $loop); + } + + /** + * @covers React\Stream\ReadableResourceStream::__construct + * @doesNotPerformAssertions + */ + public function testConstructorWithExcessiveMode() + { + // excessive flags are ignored for temp streams, so we have to use a file stream + $name = tempnam(sys_get_temp_dir(), 'test'); + $stream = @fopen($name, 'r+eANYTHING'); + unlink($name); + + $loop = $this->createLoopMock(); + $buffer = new ReadableResourceStream($stream, $loop); + $buffer->close(); + } + + /** + * @covers React\Stream\ReadableResourceStream::__construct + * @expectedException InvalidArgumentException + */ + public function testConstructorThrowsExceptionOnInvalidStream() + { + $loop = $this->createLoopMock(); + + new ReadableResourceStream(false, $loop); + } + + /** + * @covers React\Stream\ReadableResourceStream::__construct + * @expectedException InvalidArgumentException + */ + public function testConstructorThrowsExceptionOnWriteOnlyStream() + { + if (defined('HHVM_VERSION')) { + $this->markTestSkipped('HHVM does not report fopen mode for STDOUT'); + } + + $loop = $this->createLoopMock(); + + new ReadableResourceStream(STDOUT, $loop); + } + + /** + * @covers React\Stream\ReadableResourceStream::__construct + * @expectedException InvalidArgumentException + */ + public function testConstructorThrowsExceptionOnWriteOnlyStreamWithExcessiveMode() + { + // excessive flags are ignored for temp streams, so we have to use a file stream + $name = tempnam(sys_get_temp_dir(), 'test'); + $stream = fopen($name, 'weANYTHING'); + unlink($name); + + $loop = $this->createLoopMock(); + new ReadableResourceStream($stream, $loop); + } + + /** + * @covers React\Stream\ReadableResourceStream::__construct + * @expectedException RuntimeException + */ + public function testConstructorThrowsExceptionIfStreamDoesNotSupportNonBlocking() + { + if (!in_array('blocking', stream_get_wrappers())) { + stream_wrapper_register('blocking', 'React\Tests\Stream\EnforceBlockingWrapper'); + } + + $stream = fopen('blocking://test', 'r+'); + $loop = $this->createLoopMock(); + + new ReadableResourceStream($stream, $loop); + } + + + public function testCloseShouldEmitCloseEvent() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createLoopMock(); + + $conn = new ReadableResourceStream($stream, $loop); + $conn->on('close', $this->expectCallableOnce()); + + $conn->close(); + + $this->assertFalse($conn->isReadable()); + } + + public function testCloseTwiceShouldEmitCloseEventOnce() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createLoopMock(); + + $conn = new ReadableResourceStream($stream, $loop); + $conn->on('close', $this->expectCallableOnce()); + + $conn->close(); + $conn->close(); + } + + /** + * @covers React\Stream\ReadableResourceStream::__construct + * @covers React\Stream\ReadableResourceStream::handleData + */ + public function testDataEvent() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createLoopMock(); + + $capturedData = null; + + $conn = new ReadableResourceStream($stream, $loop); + $conn->on('data', function ($data) use (&$capturedData) { + $capturedData = $data; + }); + + fwrite($stream, "foobar\n"); + rewind($stream); + + $conn->handleData($stream); + $this->assertSame("foobar\n", $capturedData); + } + + /** + * @covers React\Stream\ReadableResourceStream::__construct + * @covers React\Stream\ReadableResourceStream::handleData + */ + public function testDataEventDoesEmitOneChunkMatchingBufferSize() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createLoopMock(); + + $capturedData = null; + + $conn = new ReadableResourceStream($stream, $loop, 4321); + $conn->on('data', function ($data) use (&$capturedData) { + $capturedData = $data; + }); + + fwrite($stream, str_repeat("a", 100000)); + rewind($stream); + + $conn->handleData($stream); + + $this->assertTrue($conn->isReadable()); + $this->assertEquals(4321, strlen($capturedData)); + } + + /** + * @covers React\Stream\ReadableResourceStream::__construct + * @covers React\Stream\ReadableResourceStream::handleData + */ + public function testDataEventDoesEmitOneChunkUntilStreamEndsWhenBufferSizeIsInfinite() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createLoopMock(); + + $capturedData = null; + + $conn = new ReadableResourceStream($stream, $loop, -1); + + $conn->on('data', function ($data) use (&$capturedData) { + $capturedData = $data; + }); + + fwrite($stream, str_repeat("a", 100000)); + rewind($stream); + + $conn->handleData($stream); + + $this->assertTrue($conn->isReadable()); + $this->assertEquals(100000, strlen($capturedData)); + } + + /** + * @covers React\Stream\ReadableResourceStream::handleData + */ + public function testEmptyStreamShouldNotEmitData() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createLoopMock(); + + $conn = new ReadableResourceStream($stream, $loop); + $conn->on('data', $this->expectCallableNever()); + + $conn->handleData($stream); + } + + public function testPipeShouldReturnDestination() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createLoopMock(); + + $conn = new ReadableResourceStream($stream, $loop); + $dest = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + + $this->assertSame($dest, $conn->pipe($dest)); + } + + /** + * @covers React\Stream\ReadableResourceStream::handleData + */ + public function testClosingStreamInDataEventShouldNotTriggerError() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createLoopMock(); + + $conn = new ReadableResourceStream($stream, $loop); + $conn->on('error', $this->expectCallableNever()); + $conn->on('data', function ($data) use ($conn) { + $conn->close(); + }); + + fwrite($stream, "foobar\n"); + rewind($stream); + + $conn->handleData($stream); + } + + /** + * @covers React\Stream\ReadableResourceStream::pause + */ + public function testPauseRemovesReadStreamFromLoop() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createLoopMock(); + $loop->expects($this->once())->method('addReadStream')->with($stream); + $loop->expects($this->once())->method('removeReadStream')->with($stream); + + $conn = new ReadableResourceStream($stream, $loop); + $conn->pause(); + $conn->pause(); + } + + /** + * @covers React\Stream\ReadableResourceStream::pause + */ + public function testResumeDoesAddStreamToLoopOnlyOnce() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createLoopMock(); + $loop->expects($this->once())->method('addReadStream')->with($stream); + + $conn = new ReadableResourceStream($stream, $loop); + $conn->resume(); + $conn->resume(); + } + + /** + * @covers React\Stream\ReadableResourceStream::close + */ + public function testCloseRemovesReadStreamFromLoop() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createLoopMock(); + $loop->expects($this->once())->method('addReadStream')->with($stream); + $loop->expects($this->once())->method('removeReadStream')->with($stream); + + $conn = new ReadableResourceStream($stream, $loop); + $conn->close(); + } + + /** + * @covers React\Stream\ReadableResourceStream::close + */ + public function testCloseAfterPauseRemovesReadStreamFromLoopOnce() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createLoopMock(); + $loop->expects($this->once())->method('addReadStream')->with($stream); + $loop->expects($this->once())->method('removeReadStream')->with($stream); + + $conn = new ReadableResourceStream($stream, $loop); + $conn->pause(); + $conn->close(); + } + + /** + * @covers React\Stream\ReadableResourceStream::close + */ + public function testResumeAfterCloseDoesAddReadStreamToLoopOnlyOnce() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createLoopMock(); + $loop->expects($this->once())->method('addReadStream')->with($stream); + + $conn = new ReadableResourceStream($stream, $loop); + $conn->close(); + $conn->resume(); + } + + /** + * @covers React\Stream\ReadableResourceStream::handleData + */ + public function testDataFiltered() + { + $stream = fopen('php://temp', 'r+'); + + // add a filter which removes every 'a' when reading + Filter\append($stream, function ($chunk) { + return str_replace('a', '', $chunk); + }, STREAM_FILTER_READ); + + $loop = $this->createLoopMock(); + + $capturedData = null; + + $conn = new ReadableResourceStream($stream, $loop); + $conn->on('data', function ($data) use (&$capturedData) { + $capturedData = $data; + }); + + fwrite($stream, "foobar\n"); + rewind($stream); + + $conn->handleData($stream); + $this->assertSame("foobr\n", $capturedData); + } + + /** + * @covers React\Stream\ReadableResourceStream::handleData + */ + public function testDataErrorShouldEmitErrorAndClose() + { + $stream = fopen('php://temp', 'r+'); + + // add a filter which returns an error when encountering an 'a' when reading + Filter\append($stream, function ($chunk) { + if (strpos($chunk, 'a') !== false) { + throw new \Exception('Invalid'); + } + return $chunk; + }, STREAM_FILTER_READ); + + $loop = $this->createLoopMock(); + + $conn = new ReadableResourceStream($stream, $loop); + $conn->on('data', $this->expectCallableNever()); + $conn->on('error', $this->expectCallableOnce()); + $conn->on('close', $this->expectCallableOnce()); + + fwrite($stream, "foobar\n"); + rewind($stream); + + $conn->handleData($stream); + } + + private function createLoopMock() + { + return $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + } +} diff --git a/assets/php/vendor/react/stream/tests/Stub/ReadableStreamStub.php b/assets/php/vendor/react/stream/tests/Stub/ReadableStreamStub.php new file mode 100644 index 0000000..6984f24 --- /dev/null +++ b/assets/php/vendor/react/stream/tests/Stub/ReadableStreamStub.php @@ -0,0 +1,61 @@ +<?php + +namespace React\Tests\Stream\Stub; + +use Evenement\EventEmitter; +use React\Stream\ReadableStreamInterface; +use React\Stream\WritableStreamInterface; +use React\Stream\Util; + +class ReadableStreamStub extends EventEmitter implements ReadableStreamInterface +{ + public $readable = true; + public $paused = false; + + public function isReadable() + { + return true; + } + + // trigger data event + public function write($data) + { + $this->emit('data', array($data)); + } + + // trigger error event + public function error($error) + { + $this->emit('error', array($error)); + } + + // trigger end event + public function end() + { + $this->emit('end', array()); + } + + public function pause() + { + $this->paused = true; + } + + public function resume() + { + $this->paused = false; + } + + public function close() + { + $this->readable = false; + + $this->emit('close'); + } + + public function pipe(WritableStreamInterface $dest, array $options = array()) + { + Util::pipe($this, $dest, $options); + + return $dest; + } +} diff --git a/assets/php/vendor/react/stream/tests/TestCase.php b/assets/php/vendor/react/stream/tests/TestCase.php new file mode 100644 index 0000000..c8fc1db --- /dev/null +++ b/assets/php/vendor/react/stream/tests/TestCase.php @@ -0,0 +1,54 @@ +<?php + +namespace React\Tests\Stream; + +use PHPUnit\Framework\TestCase as BaseTestCase; + +class TestCase extends BaseTestCase +{ + protected function expectCallableExactly($amount) + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->exactly($amount)) + ->method('__invoke'); + + return $mock; + } + + protected function expectCallableOnce() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke'); + + return $mock; + } + + protected function expectCallableOnceWith($value) + { + $callback = $this->createCallableMock(); + $callback + ->expects($this->once()) + ->method('__invoke') + ->with($value); + + return $callback; + } + + protected function expectCallableNever() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->never()) + ->method('__invoke'); + + return $mock; + } + + protected function createCallableMock() + { + return $this->getMockBuilder('React\Tests\Stream\CallableStub')->getMock(); + } +} diff --git a/assets/php/vendor/react/stream/tests/ThroughStreamTest.php b/assets/php/vendor/react/stream/tests/ThroughStreamTest.php new file mode 100644 index 0000000..a98badf --- /dev/null +++ b/assets/php/vendor/react/stream/tests/ThroughStreamTest.php @@ -0,0 +1,267 @@ +<?php + +namespace React\Tests\Stream; + +use React\Stream\ThroughStream; + +/** + * @covers React\Stream\ThroughStream + */ +class ThroughStreamTest extends TestCase +{ + /** + * @test + * @expectedException InvalidArgumentException + */ + public function itShouldRejectInvalidCallback() + { + new ThroughStream(123); + } + + /** @test */ + public function itShouldReturnTrueForAnyDataWrittenToIt() + { + $through = new ThroughStream(); + $ret = $through->write('foo'); + + $this->assertTrue($ret); + } + + /** @test */ + public function itShouldEmitAnyDataWrittenToIt() + { + $through = new ThroughStream(); + $through->on('data', $this->expectCallableOnceWith('foo')); + $through->write('foo'); + } + + /** @test */ + public function itShouldEmitAnyDataWrittenToItPassedThruFunction() + { + $through = new ThroughStream('strtoupper'); + $through->on('data', $this->expectCallableOnceWith('FOO')); + $through->write('foo'); + } + + /** @test */ + public function itShouldEmitAnyDataWrittenToItPassedThruCallback() + { + $through = new ThroughStream('strtoupper'); + $through->on('data', $this->expectCallableOnceWith('FOO')); + $through->write('foo'); + } + + /** @test */ + public function itShouldEmitErrorAndCloseIfCallbackThrowsException() + { + $through = new ThroughStream(function () { + throw new \RuntimeException(); + }); + $through->on('error', $this->expectCallableOnce()); + $through->on('close', $this->expectCallableOnce()); + $through->on('data', $this->expectCallableNever()); + $through->on('end', $this->expectCallableNever()); + + $through->write('foo'); + + $this->assertFalse($through->isReadable()); + $this->assertFalse($through->isWritable()); + } + + /** @test */ + public function itShouldEmitErrorAndCloseIfCallbackThrowsExceptionOnEnd() + { + $through = new ThroughStream(function () { + throw new \RuntimeException(); + }); + $through->on('error', $this->expectCallableOnce()); + $through->on('close', $this->expectCallableOnce()); + $through->on('data', $this->expectCallableNever()); + $through->on('end', $this->expectCallableNever()); + + $through->end('foo'); + + $this->assertFalse($through->isReadable()); + $this->assertFalse($through->isWritable()); + } + + /** @test */ + public function itShouldReturnFalseForAnyDataWrittenToItWhenPaused() + { + $through = new ThroughStream(); + $through->pause(); + $ret = $through->write('foo'); + + $this->assertFalse($ret); + } + + /** @test */ + public function itShouldEmitDrainOnResumeAfterReturnFalseForAnyDataWrittenToItWhenPaused() + { + $through = new ThroughStream(); + $through->pause(); + $through->write('foo'); + + $through->on('drain', $this->expectCallableOnce()); + $through->resume(); + } + + /** @test */ + public function itShouldReturnTrueForAnyDataWrittenToItWhenResumedAfterPause() + { + $through = new ThroughStream(); + $through->on('drain', $this->expectCallableNever()); + $through->pause(); + $through->resume(); + $ret = $through->write('foo'); + + $this->assertTrue($ret); + } + + /** @test */ + public function pipingStuffIntoItShouldWork() + { + $readable = new ThroughStream(); + + $through = new ThroughStream(); + $through->on('data', $this->expectCallableOnceWith('foo')); + + $readable->pipe($through); + $readable->emit('data', array('foo')); + } + + /** @test */ + public function endShouldEmitEndAndClose() + { + $through = new ThroughStream(); + $through->on('data', $this->expectCallableNever()); + $through->on('end', $this->expectCallableOnce()); + $through->on('close', $this->expectCallableOnce()); + $through->end(); + } + + /** @test */ + public function endShouldCloseTheStream() + { + $through = new ThroughStream(); + $through->on('data', $this->expectCallableNever()); + $through->end(); + + $this->assertFalse($through->isReadable()); + $this->assertFalse($through->isWritable()); + } + + /** @test */ + public function endShouldWriteDataBeforeClosing() + { + $through = new ThroughStream(); + $through->on('data', $this->expectCallableOnceWith('foo')); + $through->end('foo'); + + $this->assertFalse($through->isReadable()); + $this->assertFalse($through->isWritable()); + } + + /** @test */ + public function endTwiceShouldOnlyEmitOnce() + { + $through = new ThroughStream(); + $through->on('data', $this->expectCallableOnce('first')); + $through->end('first'); + $through->end('ignored'); + } + + /** @test */ + public function writeAfterEndShouldReturnFalse() + { + $through = new ThroughStream(); + $through->on('data', $this->expectCallableNever()); + $through->end(); + + $this->assertFalse($through->write('foo')); + } + + /** @test */ + public function writeDataWillCloseStreamShouldReturnFalse() + { + $through = new ThroughStream(); + $through->on('data', array($through, 'close')); + + $this->assertFalse($through->write('foo')); + } + + /** @test */ + public function writeDataToPausedShouldReturnFalse() + { + $through = new ThroughStream(); + $through->pause(); + + $this->assertFalse($through->write('foo')); + } + + /** @test */ + public function writeDataToResumedShouldReturnTrue() + { + $through = new ThroughStream(); + $through->pause(); + $through->resume(); + + $this->assertTrue($through->write('foo')); + } + + /** @test */ + public function itShouldBeReadableByDefault() + { + $through = new ThroughStream(); + $this->assertTrue($through->isReadable()); + } + + /** @test */ + public function itShouldBeWritableByDefault() + { + $through = new ThroughStream(); + $this->assertTrue($through->isWritable()); + } + + /** @test */ + public function closeShouldCloseOnce() + { + $through = new ThroughStream(); + + $through->on('close', $this->expectCallableOnce()); + + $through->close(); + + $this->assertFalse($through->isReadable()); + $this->assertFalse($through->isWritable()); + } + + /** @test */ + public function doubleCloseShouldCloseOnce() + { + $through = new ThroughStream(); + + $through->on('close', $this->expectCallableOnce()); + + $through->close(); + $through->close(); + + $this->assertFalse($through->isReadable()); + $this->assertFalse($through->isWritable()); + } + + /** @test */ + public function pipeShouldPipeCorrectly() + { + $output = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $output->expects($this->any())->method('isWritable')->willReturn(True); + $output + ->expects($this->once()) + ->method('write') + ->with('foo'); + + $through = new ThroughStream(); + $through->pipe($output); + $through->write('foo'); + } +} diff --git a/assets/php/vendor/react/stream/tests/UtilTest.php b/assets/php/vendor/react/stream/tests/UtilTest.php new file mode 100644 index 0000000..3d113ab --- /dev/null +++ b/assets/php/vendor/react/stream/tests/UtilTest.php @@ -0,0 +1,273 @@ +<?php + +namespace React\Tests\Stream; + +use React\Stream\WritableResourceStream; +use React\Stream\Util; +use React\Stream\CompositeStream; +use React\Stream\ThroughStream; + +/** + * @covers React\Stream\Util + */ +class UtilTest extends TestCase +{ + public function testPipeReturnsDestinationStream() + { + $readable = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + + $writable = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + + $ret = Util::pipe($readable, $writable); + + $this->assertSame($writable, $ret); + } + + public function testPipeNonReadableSourceShouldDoNothing() + { + $readable = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $readable + ->expects($this->any()) + ->method('isReadable') + ->willReturn(false); + + $writable = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $writable + ->expects($this->never()) + ->method('isWritable'); + $writable + ->expects($this->never()) + ->method('end'); + + Util::pipe($readable, $writable); + } + + public function testPipeIntoNonWritableDestinationShouldPauseSource() + { + $readable = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $readable + ->expects($this->any()) + ->method('isReadable') + ->willReturn(true); + $readable + ->expects($this->once()) + ->method('pause'); + + $writable = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $writable + ->expects($this->any()) + ->method('isWritable') + ->willReturn(false); + $writable + ->expects($this->never()) + ->method('end'); + + Util::pipe($readable, $writable); + } + + public function testPipeClosingDestPausesSource() + { + $readable = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $readable + ->expects($this->any()) + ->method('isReadable') + ->willReturn(true); + $readable + ->expects($this->once()) + ->method('pause'); + + $writable = new ThroughStream(); + + Util::pipe($readable, $writable); + + $writable->close(); + } + + public function testPipeWithEnd() + { + $readable = new Stub\ReadableStreamStub(); + + $writable = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $writable + ->expects($this->any()) + ->method('isWritable') + ->willReturn(true); + $writable + ->expects($this->once()) + ->method('end'); + + Util::pipe($readable, $writable); + + $readable->end(); + } + + public function testPipeWithoutEnd() + { + $readable = new Stub\ReadableStreamStub(); + + $writable = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $writable + ->expects($this->any()) + ->method('isWritable') + ->willReturn(true); + $writable + ->expects($this->never()) + ->method('end'); + + Util::pipe($readable, $writable, array('end' => false)); + + $readable->end(); + } + + public function testPipeWithTooSlowWritableShouldPauseReadable() + { + $readable = new Stub\ReadableStreamStub(); + + $writable = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $writable + ->expects($this->any()) + ->method('isWritable') + ->willReturn(true); + $writable + ->expects($this->once()) + ->method('write') + ->with('some data') + ->will($this->returnValue(false)); + + $readable->pipe($writable); + + $this->assertFalse($readable->paused); + $readable->write('some data'); + $this->assertTrue($readable->paused); + } + + public function testPipeWithTooSlowWritableShouldResumeOnDrain() + { + $readable = new Stub\ReadableStreamStub(); + + $onDrain = null; + + $writable = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $writable + ->expects($this->any()) + ->method('isWritable') + ->willReturn(true); + $writable + ->expects($this->any()) + ->method('on') + ->will($this->returnCallback(function ($name, $callback) use (&$onDrain) { + if ($name === 'drain') { + $onDrain = $callback; + } + })); + + $readable->pipe($writable); + $readable->pause(); + + $this->assertTrue($readable->paused); + $this->assertNotNull($onDrain); + $onDrain(); + $this->assertFalse($readable->paused); + } + + public function testPipeWithWritableResourceStream() + { + $readable = new Stub\ReadableStreamStub(); + + $stream = fopen('php://temp', 'r+'); + $loop = $this->createLoopMock(); + $buffer = new WritableResourceStream($stream, $loop); + + $readable->pipe($buffer); + + $readable->write('hello, I am some '); + $readable->write('random data'); + + $buffer->handleWrite(); + rewind($stream); + $this->assertSame('hello, I am some random data', stream_get_contents($stream)); + } + + public function testPipeSetsUpListeners() + { + $source = new ThroughStream(); + $dest = new ThroughStream(); + + $this->assertCount(0, $source->listeners('data')); + $this->assertCount(0, $source->listeners('end')); + $this->assertCount(0, $dest->listeners('drain')); + + Util::pipe($source, $dest); + + $this->assertCount(1, $source->listeners('data')); + $this->assertCount(1, $source->listeners('end')); + $this->assertCount(1, $dest->listeners('drain')); + } + + public function testPipeClosingSourceRemovesListeners() + { + $source = new ThroughStream(); + $dest = new ThroughStream(); + + Util::pipe($source, $dest); + + $source->close(); + + $this->assertCount(0, $source->listeners('data')); + $this->assertCount(0, $source->listeners('end')); + $this->assertCount(0, $dest->listeners('drain')); + } + + public function testPipeClosingDestRemovesListeners() + { + $source = new ThroughStream(); + $dest = new ThroughStream(); + + Util::pipe($source, $dest); + + $dest->close(); + + $this->assertCount(0, $source->listeners('data')); + $this->assertCount(0, $source->listeners('end')); + $this->assertCount(0, $dest->listeners('drain')); + } + + public function testPipeDuplexIntoSelfEndsOnEnd() + { + $readable = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $readable->expects($this->any())->method('isReadable')->willReturn(true); + $writable = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $writable->expects($this->any())->method('isWritable')->willReturn(true); + $duplex = new CompositeStream($readable, $writable); + + Util::pipe($duplex, $duplex); + + $writable->expects($this->once())->method('end'); + + $duplex->emit('end'); + } + + /** @test */ + public function forwardEventsShouldSetupForwards() + { + $source = new ThroughStream(); + $target = new ThroughStream(); + + Util::forwardEvents($source, $target, array('data')); + $target->on('data', $this->expectCallableOnce()); + $target->on('foo', $this->expectCallableNever()); + + $source->emit('data', array('hello')); + $source->emit('foo', array('bar')); + } + + private function createLoopMock() + { + return $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + } + + private function notEqualTo($value) + { + return new \PHPUnit_Framework_Constraint_Not($value); + } +} diff --git a/assets/php/vendor/react/stream/tests/WritableStreamResourceTest.php b/assets/php/vendor/react/stream/tests/WritableStreamResourceTest.php new file mode 100644 index 0000000..05bce9c --- /dev/null +++ b/assets/php/vendor/react/stream/tests/WritableStreamResourceTest.php @@ -0,0 +1,534 @@ +<?php + +namespace React\Tests\Stream; + +use Clue\StreamFilter as Filter; +use React\Stream\WritableResourceStream; + +class WritableResourceStreamTest extends TestCase +{ + /** + * @covers React\Stream\WritableResourceStream::__construct + * @doesNotPerformAssertions + */ + public function testConstructor() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createLoopMock(); + + new WritableResourceStream($stream, $loop); + } + + /** + * @covers React\Stream\WritableResourceStream::__construct + * @doesNotPerformAssertions + */ + public function testConstructorWithExcessiveMode() + { + // excessive flags are ignored for temp streams, so we have to use a file stream + $name = tempnam(sys_get_temp_dir(), 'test'); + $stream = @fopen($name, 'w+eANYTHING'); + unlink($name); + + $loop = $this->createLoopMock(); + $buffer = new WritableResourceStream($stream, $loop); + $buffer->close(); + } + + /** + * @covers React\Stream\WritableResourceStream::__construct + * @expectedException InvalidArgumentException + */ + public function testConstructorThrowsIfNotAValidStreamResource() + { + $stream = null; + $loop = $this->createLoopMock(); + + new WritableResourceStream($stream, $loop); + } + + /** + * @covers React\Stream\WritableResourceStream::__construct + * @expectedException InvalidArgumentException + */ + public function testConstructorThrowsExceptionOnReadOnlyStream() + { + $stream = fopen('php://temp', 'r'); + $loop = $this->createLoopMock(); + + new WritableResourceStream($stream, $loop); + } + + /** + * @covers React\Stream\WritableResourceStream::__construct + * @expectedException InvalidArgumentException + */ + public function testConstructorThrowsExceptionOnReadOnlyStreamWithExcessiveMode() + { + // excessive flags are ignored for temp streams, so we have to use a file stream + $name = tempnam(sys_get_temp_dir(), 'test'); + $stream = fopen($name, 'reANYTHING'); + unlink($name); + + $loop = $this->createLoopMock(); + new WritableResourceStream($stream, $loop); + } + + /** + * @covers React\Stream\WritableResourceStream::__construct + * @expectedException RuntimeException + */ + public function testConstructorThrowsExceptionIfStreamDoesNotSupportNonBlocking() + { + if (!in_array('blocking', stream_get_wrappers())) { + stream_wrapper_register('blocking', 'React\Tests\Stream\EnforceBlockingWrapper'); + } + + $stream = fopen('blocking://test', 'r+'); + $loop = $this->createLoopMock(); + + new WritableResourceStream($stream, $loop); + } + + /** + * @covers React\Stream\WritableResourceStream::write + * @covers React\Stream\WritableResourceStream::handleWrite + */ + public function testWrite() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createWriteableLoopMock(); + + $buffer = new WritableResourceStream($stream, $loop); + $buffer->on('error', $this->expectCallableNever()); + + $buffer->write("foobar\n"); + rewind($stream); + $this->assertSame("foobar\n", fread($stream, 1024)); + } + + /** + * @covers React\Stream\WritableResourceStream::write + */ + public function testWriteWithDataDoesAddResourceToLoop() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createLoopMock(); + $loop->expects($this->once())->method('addWriteStream')->with($this->equalTo($stream)); + + $buffer = new WritableResourceStream($stream, $loop); + + $buffer->write("foobar\n"); + } + + /** + * @covers React\Stream\WritableResourceStream::write + * @covers React\Stream\WritableResourceStream::handleWrite + */ + public function testEmptyWriteDoesNotAddToLoop() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createLoopMock(); + $loop->expects($this->never())->method('addWriteStream'); + + $buffer = new WritableResourceStream($stream, $loop); + + $buffer->write(""); + $buffer->write(null); + } + + /** + * @covers React\Stream\WritableResourceStream::write + * @covers React\Stream\WritableResourceStream::handleWrite + */ + public function testWriteReturnsFalseWhenWritableResourceStreamIsFull() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createWriteableLoopMock(); + $loop->preventWrites = true; + + $buffer = new WritableResourceStream($stream, $loop, 4); + $buffer->on('error', $this->expectCallableNever()); + + $this->assertTrue($buffer->write("foo")); + $loop->preventWrites = false; + $this->assertFalse($buffer->write("bar\n")); + } + + /** + * @covers React\Stream\WritableResourceStream::write + */ + public function testWriteReturnsFalseWhenWritableResourceStreamIsExactlyFull() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createLoopMock(); + + $buffer = new WritableResourceStream($stream, $loop, 3); + + $this->assertFalse($buffer->write("foo")); + } + + /** + * @covers React\Stream\WritableResourceStream::write + * @covers React\Stream\WritableResourceStream::handleWrite + */ + public function testWriteDetectsWhenOtherSideIsClosed() + { + list($a, $b) = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP); + + $loop = $this->createWriteableLoopMock(); + + $buffer = new WritableResourceStream($a, $loop, 4); + $buffer->on('error', $this->expectCallableOnce()); + + fclose($b); + + $buffer->write("foo"); + } + + /** + * @covers React\Stream\WritableResourceStream::write + * @covers React\Stream\WritableResourceStream::handleWrite + */ + public function testEmitsDrainAfterWriteWhichExceedsBuffer() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createLoopMock(); + + $buffer = new WritableResourceStream($stream, $loop, 2); + $buffer->on('error', $this->expectCallableNever()); + $buffer->on('drain', $this->expectCallableOnce()); + + $buffer->write("foo"); + $buffer->handleWrite(); + } + + /** + * @covers React\Stream\WritableResourceStream::write + * @covers React\Stream\WritableResourceStream::handleWrite + */ + public function testWriteInDrain() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createLoopMock(); + + $buffer = new WritableResourceStream($stream, $loop, 2); + $buffer->on('error', $this->expectCallableNever()); + + $buffer->once('drain', function () use ($buffer) { + $buffer->write("bar\n"); + $buffer->handleWrite(); + }); + + $this->assertFalse($buffer->write("foo\n")); + $buffer->handleWrite(); + + fseek($stream, 0); + $this->assertSame("foo\nbar\n", stream_get_contents($stream)); + } + + /** + * @covers React\Stream\WritableResourceStream::write + * @covers React\Stream\WritableResourceStream::handleWrite + */ + public function testDrainAfterWrite() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createLoopMock(); + + $buffer = new WritableResourceStream($stream, $loop, 2); + + $buffer->on('drain', $this->expectCallableOnce()); + + $buffer->write("foo"); + $buffer->handleWrite(); + } + + /** + * @covers React\Stream\WritableResourceStream::handleWrite + */ + public function testDrainAfterWriteWillRemoveResourceFromLoopWithoutClosing() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createLoopMock(); + $loop->expects($this->once())->method('removeWriteStream')->with($stream); + + $buffer = new WritableResourceStream($stream, $loop, 2); + + $buffer->on('drain', $this->expectCallableOnce()); + + $buffer->on('close', $this->expectCallableNever()); + + $buffer->write("foo"); + $buffer->handleWrite(); + } + + /** + * @covers React\Stream\WritableResourceStream::handleWrite + */ + public function testClosingDuringDrainAfterWriteWillRemoveResourceFromLoopOnceAndClose() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createLoopMock(); + $loop->expects($this->once())->method('removeWriteStream')->with($stream); + + $buffer = new WritableResourceStream($stream, $loop, 2); + + $buffer->on('drain', function () use ($buffer) { + $buffer->close(); + }); + + $buffer->on('close', $this->expectCallableOnce()); + + $buffer->write("foo"); + $buffer->handleWrite(); + } + + /** + * @covers React\Stream\WritableResourceStream::end + */ + public function testEndWithoutDataClosesImmediatelyIfWritableResourceStreamIsEmpty() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createLoopMock(); + + $buffer = new WritableResourceStream($stream, $loop); + $buffer->on('error', $this->expectCallableNever()); + $buffer->on('close', $this->expectCallableOnce()); + + $this->assertTrue($buffer->isWritable()); + $buffer->end(); + $this->assertFalse($buffer->isWritable()); + } + + /** + * @covers React\Stream\WritableResourceStream::end + */ + public function testEndWithoutDataDoesNotCloseIfWritableResourceStreamIsFull() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createLoopMock(); + + $buffer = new WritableResourceStream($stream, $loop); + $buffer->on('error', $this->expectCallableNever()); + $buffer->on('close', $this->expectCallableNever()); + + $buffer->write('foo'); + + $this->assertTrue($buffer->isWritable()); + $buffer->end(); + $this->assertFalse($buffer->isWritable()); + } + + /** + * @covers React\Stream\WritableResourceStream::end + */ + public function testEndWithDataClosesImmediatelyIfWritableResourceStreamFlushes() + { + $stream = fopen('php://temp', 'r+'); + $filterBuffer = ''; + $loop = $this->createLoopMock(); + + $buffer = new WritableResourceStream($stream, $loop); + $buffer->on('error', $this->expectCallableNever()); + $buffer->on('close', $this->expectCallableOnce()); + + Filter\append($stream, function ($chunk) use (&$filterBuffer) { + $filterBuffer .= $chunk; + return $chunk; + }); + + $this->assertTrue($buffer->isWritable()); + $buffer->end('final words'); + $this->assertFalse($buffer->isWritable()); + + $buffer->handleWrite(); + $this->assertSame('final words', $filterBuffer); + } + + /** + * @covers React\Stream\WritableResourceStream::end + */ + public function testEndWithDataDoesNotCloseImmediatelyIfWritableResourceStreamIsFull() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createLoopMock(); + + $buffer = new WritableResourceStream($stream, $loop); + $buffer->on('error', $this->expectCallableNever()); + $buffer->on('close', $this->expectCallableNever()); + + $buffer->write('foo'); + + $this->assertTrue($buffer->isWritable()); + $buffer->end('final words'); + $this->assertFalse($buffer->isWritable()); + + rewind($stream); + $this->assertSame('', stream_get_contents($stream)); + } + + /** + * @covers React\Stream\WritableResourceStream::isWritable + * @covers React\Stream\WritableResourceStream::close + */ + public function testClose() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createLoopMock(); + + $buffer = new WritableResourceStream($stream, $loop); + $buffer->on('error', $this->expectCallableNever()); + $buffer->on('close', $this->expectCallableOnce()); + + $this->assertTrue($buffer->isWritable()); + $buffer->close(); + $this->assertFalse($buffer->isWritable()); + + $this->assertEquals(array(), $buffer->listeners('close')); + } + + /** + * @covers React\Stream\WritableResourceStream::close + */ + public function testClosingAfterWriteRemovesStreamFromLoop() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createLoopMock(); + $buffer = new WritableResourceStream($stream, $loop); + + $loop->expects($this->once())->method('removeWriteStream')->with($stream); + + $buffer->write('foo'); + $buffer->close(); + } + + /** + * @covers React\Stream\WritableResourceStream::close + */ + public function testClosingWithoutWritingDoesNotRemoveStreamFromLoop() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createLoopMock(); + $buffer = new WritableResourceStream($stream, $loop); + + $loop->expects($this->never())->method('removeWriteStream'); + + $buffer->close(); + } + + /** + * @covers React\Stream\WritableResourceStream::close + */ + public function testDoubleCloseWillEmitOnlyOnce() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createLoopMock(); + + $buffer = new WritableResourceStream($stream, $loop); + $buffer->on('close', $this->expectCallableOnce()); + + $buffer->close(); + $buffer->close(); + } + + /** + * @covers React\Stream\WritableResourceStream::write + * @covers React\Stream\WritableResourceStream::close + */ + public function testWritingToClosedWritableResourceStreamShouldNotWriteToStream() + { + $stream = fopen('php://temp', 'r+'); + $filterBuffer = ''; + $loop = $this->createLoopMock(); + + $buffer = new WritableResourceStream($stream, $loop); + + Filter\append($stream, function ($chunk) use (&$filterBuffer) { + $filterBuffer .= $chunk; + return $chunk; + }); + + $buffer->close(); + + $buffer->write('foo'); + + $buffer->handleWrite(); + $this->assertSame('', $filterBuffer); + } + + /** + * @covers React\Stream\WritableResourceStream::handleWrite + */ + public function testErrorWhenStreamResourceIsInvalid() + { + $stream = fopen('php://temp', 'r+'); + $loop = $this->createWriteableLoopMock(); + + $error = null; + + $buffer = new WritableResourceStream($stream, $loop); + $buffer->on('error', function ($message) use (&$error) { + $error = $message; + }); + + // invalidate stream resource + fclose($stream); + + $buffer->write('Attempting to write to bad stream'); + + $this->assertInstanceOf('Exception', $error); + + // the error messages differ between PHP versions, let's just check substrings + $this->assertContains('Unable to write to stream: ', $error->getMessage()); + $this->assertContains(' not a valid stream resource', $error->getMessage(), '', true); + } + + public function testWritingToClosedStream() + { + if ('Darwin' === PHP_OS) { + $this->markTestSkipped('OS X issue with shutting down pair for writing'); + } + + list($a, $b) = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP); + $loop = $this->createLoopMock(); + + $error = null; + + $buffer = new WritableResourceStream($a, $loop); + $buffer->on('error', function($message) use (&$error) { + $error = $message; + }); + + $buffer->write('foo'); + $buffer->handleWrite(); + stream_socket_shutdown($b, STREAM_SHUT_RD); + stream_socket_shutdown($a, STREAM_SHUT_RD); + $buffer->write('bar'); + $buffer->handleWrite(); + + $this->assertInstanceOf('Exception', $error); + $this->assertSame('Unable to write to stream: fwrite(): send of 3 bytes failed with errno=32 Broken pipe', $error->getMessage()); + } + + private function createWriteableLoopMock() + { + $loop = $this->createLoopMock(); + $loop->preventWrites = false; + $loop + ->expects($this->any()) + ->method('addWriteStream') + ->will($this->returnCallback(function ($stream, $listener) use ($loop) { + if (!$loop->preventWrites) { + call_user_func($listener, $stream); + } + })); + + return $loop; + } + + private function createLoopMock() + { + return $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + } +} |