Version v0.12.0-dev
This commit is contained in:
parent
adb56cbe0a
commit
85d9e00ec3
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
// autoload.php @generated by Composer
|
||||
|
||||
if (PHP_VERSION_ID < 50600) {
|
||||
if (!headers_sent()) {
|
||||
header('HTTP/1.1 500 Internal Server Error');
|
||||
}
|
||||
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
|
||||
if (!ini_get('display_errors')) {
|
||||
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
|
||||
fwrite(STDERR, $err);
|
||||
} elseif (!headers_sent()) {
|
||||
echo $err;
|
||||
}
|
||||
}
|
||||
trigger_error(
|
||||
$err,
|
||||
E_USER_ERROR
|
||||
);
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/composer/autoload_real.php';
|
||||
|
||||
return ComposerAutoloaderInit90058a67762ccdd1948ae1961aa43e94::getLoader();
|
|
@ -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.
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"name": "clue/block-react",
|
||||
"description": "Lightweight library that eases integrating async components built for ReactPHP in a traditional, blocking environment.",
|
||||
"keywords": ["blocking", "await", "sleep", "Event Loop", "synchronous", "Promise", "ReactPHP", "async"],
|
||||
"homepage": "https://github.com/clue/reactphp-block",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Christian Lück",
|
||||
"email": "christian@clue.engineering"
|
||||
}
|
||||
],
|
||||
"autoload": {
|
||||
"files": [ "src/functions_include.php" ]
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": { "Clue\\Tests\\React\\Block\\": "tests/" }
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3",
|
||||
"react/event-loop": "^1.2",
|
||||
"react/promise": "^3.0 || ^2.7 || ^1.2.1",
|
||||
"react/promise-timer": "^1.5"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35",
|
||||
"react/http": "^1.4"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,357 @@
|
|||
<?php
|
||||
|
||||
namespace Clue\React\Block;
|
||||
|
||||
use React\EventLoop\Loop;
|
||||
use React\EventLoop\LoopInterface;
|
||||
use React\Promise;
|
||||
use React\Promise\CancellablePromiseInterface;
|
||||
use React\Promise\PromiseInterface;
|
||||
use React\Promise\Timer;
|
||||
use React\Promise\Timer\TimeoutException;
|
||||
use Exception;
|
||||
use UnderflowException;
|
||||
|
||||
/**
|
||||
* Wait/sleep for `$time` seconds.
|
||||
*
|
||||
* ```php
|
||||
* Clue\React\Block\sleep(1.5, $loop);
|
||||
* ```
|
||||
*
|
||||
* This function will only return after the given `$time` has elapsed. In the
|
||||
* meantime, the event loop will run any other events attached to the same loop
|
||||
* until the timer fires. If there are no other events attached to this loop,
|
||||
* it will behave similar to the built-in [`sleep()`](https://www.php.net/manual/en/function.sleep.php).
|
||||
*
|
||||
* Internally, the `$time` argument will be used as a timer for the loop so that
|
||||
* it keeps running until this timer 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 takes an optional `LoopInterface|null $loop` parameter that can be used to
|
||||
* pass the event loop instance to use. You can use a `null` value here in order to
|
||||
* use the [default loop](https://github.com/reactphp/event-loop#loop). This value
|
||||
* SHOULD NOT be given unless you're sure you want to explicitly use a given event
|
||||
* loop instance.
|
||||
*
|
||||
* Note that this function will assume control over the event loop. Internally, it
|
||||
* will actually `run()` the loop until the timer fires and then calls `stop()` to
|
||||
* terminate execution of the loop. This means this function is more suited for
|
||||
* short-lived program executions when using async APIs is not feasible. For
|
||||
* long-running applications, using event-driven APIs by leveraging timers
|
||||
* is usually preferable.
|
||||
*
|
||||
* @param float $time
|
||||
* @param ?LoopInterface $loop
|
||||
* @return void
|
||||
*/
|
||||
function sleep($time, LoopInterface $loop = null)
|
||||
{
|
||||
await(Timer\resolve($time, $loop), $loop);
|
||||
}
|
||||
|
||||
/**
|
||||
* Block waiting for the given `$promise` to be fulfilled.
|
||||
*
|
||||
* ```php
|
||||
* $result = Clue\React\Block\await($promise, $loop);
|
||||
* ```
|
||||
*
|
||||
* This function will only return after the given `$promise` has settled, i.e.
|
||||
* either fulfilled or rejected. In the meantime, the event loop will run any
|
||||
* events attached to the same loop until the promise settles.
|
||||
*
|
||||
* Once the promise is fulfilled, this function will return whatever the promise
|
||||
* resolved to.
|
||||
*
|
||||
* Once the promise is rejected, this will throw whatever the promise rejected
|
||||
* with. If the promise did not reject with an `Exception`, then this function
|
||||
* will throw an `UnexpectedValueException` instead.
|
||||
*
|
||||
* ```php
|
||||
* try {
|
||||
* $result = Clue\React\Block\await($promise, $loop);
|
||||
* // promise successfully fulfilled with $result
|
||||
* echo 'Result: ' . $result;
|
||||
* } catch (Exception $exception) {
|
||||
* // promise rejected with $exception
|
||||
* echo 'ERROR: ' . $exception->getMessage();
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* See also the [examples](../examples/).
|
||||
*
|
||||
* This function takes an optional `LoopInterface|null $loop` parameter that can be used to
|
||||
* pass the event loop instance to use. You can use a `null` value here in order to
|
||||
* use the [default loop](https://github.com/reactphp/event-loop#loop). This value
|
||||
* SHOULD NOT be given unless you're sure you want to explicitly use a given event
|
||||
* loop instance.
|
||||
*
|
||||
* If no `$timeout` argument is given and the promise stays pending, then this
|
||||
* will potentially wait/block forever until the promise is settled. To avoid
|
||||
* this, API authors creating promises are expected to provide means to
|
||||
* configure a timeout for the promise instead. For more details, see also the
|
||||
* [`timeout()` function](https://github.com/reactphp/promise-timer#timeout).
|
||||
*
|
||||
* If the deprecated `$timeout` argument is given and the promise is still pending once the
|
||||
* timeout triggers, this will `cancel()` the promise and throw a `TimeoutException`.
|
||||
* 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.
|
||||
*
|
||||
* Note that this function will assume control over the event loop. Internally, it
|
||||
* will actually `run()` the loop until the promise settles and then calls `stop()` to
|
||||
* terminate execution of the loop. This means this function is more suited for
|
||||
* short-lived promise executions when using promise-based APIs is not feasible.
|
||||
* For long-running applications, using promise-based APIs by leveraging chained
|
||||
* `then()` calls is usually preferable.
|
||||
*
|
||||
* @param PromiseInterface $promise
|
||||
* @param ?LoopInterface $loop
|
||||
* @param ?float $timeout [deprecated] (optional) maximum timeout in seconds or null=wait forever
|
||||
* @return mixed returns whatever the promise resolves to
|
||||
* @throws Exception when the promise is rejected
|
||||
* @throws TimeoutException if the $timeout is given and triggers
|
||||
*/
|
||||
function await(PromiseInterface $promise, LoopInterface $loop = null, $timeout = null)
|
||||
{
|
||||
$wait = true;
|
||||
$resolved = null;
|
||||
$exception = null;
|
||||
$rejected = false;
|
||||
$loop = $loop ?: Loop::get();
|
||||
|
||||
if ($timeout !== null) {
|
||||
$promise = Timer\timeout($promise, $timeout, $loop);
|
||||
}
|
||||
|
||||
$promise->then(
|
||||
function ($c) use (&$resolved, &$wait, $loop) {
|
||||
$resolved = $c;
|
||||
$wait = false;
|
||||
$loop->stop();
|
||||
},
|
||||
function ($error) use (&$exception, &$rejected, &$wait, $loop) {
|
||||
$exception = $error;
|
||||
$rejected = true;
|
||||
$wait = false;
|
||||
$loop->stop();
|
||||
}
|
||||
);
|
||||
|
||||
// Explicitly overwrite argument with null value. This ensure that this
|
||||
// argument does not show up in the stack trace in PHP 7+ only.
|
||||
$promise = null;
|
||||
|
||||
while ($wait) {
|
||||
$loop->run();
|
||||
}
|
||||
|
||||
if ($rejected) {
|
||||
if (!$exception instanceof \Exception && !$exception instanceof \Throwable) {
|
||||
$exception = new \UnexpectedValueException(
|
||||
'Promise rejected with unexpected value of type ' . (is_object($exception) ? get_class($exception) : gettype($exception))
|
||||
);
|
||||
} elseif (!$exception instanceof \Exception) {
|
||||
$exception = new \UnexpectedValueException(
|
||||
'Promise rejected with unexpected ' . get_class($exception) . ': ' . $exception->getMessage(),
|
||||
$exception->getCode(),
|
||||
$exception
|
||||
);
|
||||
}
|
||||
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
return $resolved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for ANY of the given promises to be fulfilled.
|
||||
*
|
||||
* ```php
|
||||
* $promises = array(
|
||||
* $promise1,
|
||||
* $promise2
|
||||
* );
|
||||
*
|
||||
* $firstResult = Clue\React\Block\awaitAny($promises, $loop);
|
||||
*
|
||||
* echo 'First result: ' . $firstResult;
|
||||
* ```
|
||||
*
|
||||
* See also the [examples](../examples/).
|
||||
*
|
||||
* This function will only return after ANY of the given `$promises` has been
|
||||
* fulfilled or will throw when ALL of them have been rejected. In the meantime,
|
||||
* the event loop will run any events attached to the same loop.
|
||||
*
|
||||
* Once ANY promise is fulfilled, this function will return whatever this
|
||||
* promise resolved to and will try to `cancel()` all remaining promises.
|
||||
*
|
||||
* Once ALL promises reject, this function will fail and throw an `UnderflowException`.
|
||||
* Likewise, this will throw if an empty array of `$promises` is passed.
|
||||
*
|
||||
* This function takes an optional `LoopInterface|null $loop` parameter that can be used to
|
||||
* pass the event loop instance to use. You can use a `null` value here in order to
|
||||
* use the [default loop](https://github.com/reactphp/event-loop#loop). This value
|
||||
* SHOULD NOT be given unless you're sure you want to explicitly use a given event
|
||||
* loop instance.
|
||||
*
|
||||
* If no `$timeout` argument is given and ALL promises stay pending, then this
|
||||
* will potentially wait/block forever until the promise is fulfilled. To avoid
|
||||
* this, API authors creating promises are expected to provide means to
|
||||
* configure a timeout for the promise instead. For more details, see also the
|
||||
* [`timeout()` function](https://github.com/reactphp/promise-timer#timeout).
|
||||
*
|
||||
* If the deprecated `$timeout` argument is given and ANY promises are still pending once
|
||||
* the timeout triggers, this will `cancel()` all pending promises and throw a
|
||||
* `TimeoutException`. 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.
|
||||
*
|
||||
* Note that this function will assume control over the event loop. Internally, it
|
||||
* will actually `run()` the loop until the promise settles and then calls `stop()` to
|
||||
* terminate execution of the loop. This means this function is more suited for
|
||||
* short-lived promise executions when using promise-based APIs is not feasible.
|
||||
* For long-running applications, using promise-based APIs by leveraging chained
|
||||
* `then()` calls is usually preferable.
|
||||
*
|
||||
* @param PromiseInterface[] $promises
|
||||
* @param ?LoopInterface $loop
|
||||
* @param ?float $timeout [deprecated] (optional) maximum timeout in seconds or null=wait forever
|
||||
* @return mixed returns whatever the first promise resolves to
|
||||
* @throws Exception if ALL promises are rejected
|
||||
* @throws TimeoutException if the $timeout is given and triggers
|
||||
*/
|
||||
function awaitAny(array $promises, LoopInterface $loop = null, $timeout = null)
|
||||
{
|
||||
// Explicitly overwrite argument with null value. This ensure that this
|
||||
// argument does not show up in the stack trace in PHP 7+ only.
|
||||
$all = $promises;
|
||||
$promises = null;
|
||||
|
||||
try {
|
||||
// Promise\any() does not cope with an empty input array, so reject this here
|
||||
if (!$all) {
|
||||
throw new UnderflowException('Empty input array');
|
||||
}
|
||||
|
||||
$ret = await(Promise\any($all)->then(null, function () {
|
||||
// rejects with an array of rejection reasons => reject with Exception instead
|
||||
throw new Exception('All promises rejected');
|
||||
}), $loop, $timeout);
|
||||
} catch (TimeoutException $e) {
|
||||
// the timeout fired
|
||||
// => try to cancel all promises (rejected ones will be ignored anyway)
|
||||
_cancelAllPromises($all);
|
||||
|
||||
throw $e;
|
||||
} catch (Exception $e) {
|
||||
// if the above throws, then ALL promises are already rejected
|
||||
// => try to cancel all promises (rejected ones will be ignored anyway)
|
||||
_cancelAllPromises($all);
|
||||
|
||||
throw new UnderflowException('No promise could resolve', 0, $e);
|
||||
}
|
||||
|
||||
// if we reach this, then ANY of the given promises resolved
|
||||
// => try to cancel all promises (settled ones will be ignored anyway)
|
||||
_cancelAllPromises($all);
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for ALL of the given promises to be fulfilled.
|
||||
*
|
||||
* ```php
|
||||
* $promises = array(
|
||||
* $promise1,
|
||||
* $promise2
|
||||
* );
|
||||
*
|
||||
* $allResults = Clue\React\Block\awaitAll($promises, $loop);
|
||||
*
|
||||
* echo 'First promise resolved with: ' . $allResults[0];
|
||||
* ```
|
||||
*
|
||||
* See also the [examples](../examples/).
|
||||
*
|
||||
* This function will only return after ALL of the given `$promises` have been
|
||||
* fulfilled or will throw when ANY of them have been rejected. In the meantime,
|
||||
* the event loop will run any events attached to the same loop.
|
||||
*
|
||||
* Once ALL promises are fulfilled, this will return an array with whatever
|
||||
* each promise resolves to. Array keys will be left intact, i.e. they can
|
||||
* be used to correlate the return array to the promises passed.
|
||||
*
|
||||
* Once ANY promise rejects, this will try to `cancel()` all remaining promises
|
||||
* and throw an `Exception`. If the promise did not reject with an `Exception`,
|
||||
* then this function will throw an `UnexpectedValueException` instead.
|
||||
*
|
||||
* This function takes an optional `LoopInterface|null $loop` parameter that can be used to
|
||||
* pass the event loop instance to use. You can use a `null` value here in order to
|
||||
* use the [default loop](https://github.com/reactphp/event-loop#loop). This value
|
||||
* SHOULD NOT be given unless you're sure you want to explicitly use a given event
|
||||
* loop instance.
|
||||
*
|
||||
* If no `$timeout` argument is given and ANY promises stay pending, then this
|
||||
* will potentially wait/block forever until the promise is fulfilled. To avoid
|
||||
* this, API authors creating promises are expected to provide means to
|
||||
* configure a timeout for the promise instead. For more details, see also the
|
||||
* [`timeout()` function](https://github.com/reactphp/promise-timer#timeout).
|
||||
*
|
||||
* If the deprecated `$timeout` argument is given and ANY promises are still pending once
|
||||
* the timeout triggers, this will `cancel()` all pending promises and throw a
|
||||
* `TimeoutException`. 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.
|
||||
*
|
||||
* Note that this function will assume control over the event loop. Internally, it
|
||||
* will actually `run()` the loop until the promise settles and then calls `stop()` to
|
||||
* terminate execution of the loop. This means this function is more suited for
|
||||
* short-lived promise executions when using promise-based APIs is not feasible.
|
||||
* For long-running applications, using promise-based APIs by leveraging chained
|
||||
* `then()` calls is usually preferable.
|
||||
*
|
||||
* @param PromiseInterface[] $promises
|
||||
* @param ?LoopInterface $loop
|
||||
* @param ?float $timeout [deprecated] (optional) maximum timeout in seconds or null=wait forever
|
||||
* @return array returns an array with whatever each promise resolves to
|
||||
* @throws Exception when ANY promise is rejected
|
||||
* @throws TimeoutException if the $timeout is given and triggers
|
||||
*/
|
||||
function awaitAll(array $promises, LoopInterface $loop = null, $timeout = null)
|
||||
{
|
||||
// Explicitly overwrite argument with null value. This ensure that this
|
||||
// argument does not show up in the stack trace in PHP 7+ only.
|
||||
$all = $promises;
|
||||
$promises = null;
|
||||
|
||||
try {
|
||||
return await(Promise\all($all), $loop, $timeout);
|
||||
} catch (Exception $e) {
|
||||
// ANY of the given promises rejected or the timeout fired
|
||||
// => try to cancel all promises (rejected ones will be ignored anyway)
|
||||
_cancelAllPromises($all);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* internal helper function used to iterate over an array of Promise instances and cancel() each
|
||||
*
|
||||
* @internal
|
||||
* @param array $promises
|
||||
* @return void
|
||||
*/
|
||||
function _cancelAllPromises(array $promises)
|
||||
{
|
||||
foreach ($promises as $promise) {
|
||||
if ($promise instanceof PromiseInterface && ($promise instanceof CancellablePromiseInterface || !\interface_exists('React\Promise\CancellablePromiseInterface'))) {
|
||||
$promise->cancel();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
namespace Clue\React\Block;
|
||||
|
||||
if (!function_exists('Clue\\React\\Block\\sleep')) {
|
||||
require __DIR__ . '/functions.php';
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013 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.
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"name": "clue/buzz-react",
|
||||
"description": "Simple, async PSR-7 HTTP client for concurrently processing any number of HTTP requests, built on top of ReactPHP",
|
||||
"keywords": ["HTTP client", "PSR-7", "HTTP", "ReactPHP", "async"],
|
||||
"homepage": "https://github.com/clue/reactphp-buzz",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Christian Lück",
|
||||
"email": "christian@clue.engineering"
|
||||
}
|
||||
],
|
||||
"autoload": {
|
||||
"psr-4": { "Clue\\React\\Buzz\\": "src/" }
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": { "Clue\\Tests\\React\\Buzz\\": "tests/" }
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3",
|
||||
"psr/http-message": "^1.0",
|
||||
"react/event-loop": "^1.0 || ^0.5",
|
||||
"react/http-client": "^0.5.10",
|
||||
"react/promise": "^2.2.1 || ^1.2.1",
|
||||
"react/promise-stream": "^1.0 || ^0.1.2",
|
||||
"react/socket": "^1.1",
|
||||
"react/stream": "^1.0 || ^0.7",
|
||||
"ringcentral/psr7": "^1.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"clue/block-react": "^1.0",
|
||||
"clue/http-proxy-react": "^1.3",
|
||||
"clue/reactphp-ssh-proxy": "^1.0",
|
||||
"clue/socks-react": "^1.0",
|
||||
"phpunit/phpunit": "^9.0 || ^5.7 || ^4.8.35",
|
||||
"react/http": "^0.8"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,867 @@
|
|||
<?php
|
||||
|
||||
namespace Clue\React\Buzz;
|
||||
|
||||
use Clue\React\Buzz\Io\Sender;
|
||||
use Clue\React\Buzz\Io\Transaction;
|
||||
use Clue\React\Buzz\Message\MessageFactory;
|
||||
use InvalidArgumentException;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
use React\EventLoop\LoopInterface;
|
||||
use React\Promise\PromiseInterface;
|
||||
use React\Socket\ConnectorInterface;
|
||||
use React\Stream\ReadableStreamInterface;
|
||||
|
||||
class Browser
|
||||
{
|
||||
private $transaction;
|
||||
private $messageFactory;
|
||||
private $baseUrl;
|
||||
private $protocolVersion = '1.1';
|
||||
|
||||
/**
|
||||
* The `Browser` is responsible for sending HTTP requests to your HTTP server
|
||||
* and keeps track of pending incoming HTTP responses.
|
||||
* It also registers everything with the main [`EventLoop`](https://github.com/reactphp/event-loop#usage).
|
||||
*
|
||||
* ```php
|
||||
* $loop = React\EventLoop\Factory::create();
|
||||
*
|
||||
* $browser = new Clue\React\Buzz\Browser($loop);
|
||||
* ```
|
||||
*
|
||||
* If you need custom connector settings (DNS resolution, TLS parameters, timeouts,
|
||||
* proxy servers etc.), you can explicitly pass a custom instance of the
|
||||
* [`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface):
|
||||
*
|
||||
* ```php
|
||||
* $connector = new React\Socket\Connector($loop, array(
|
||||
* 'dns' => '127.0.0.1',
|
||||
* 'tcp' => array(
|
||||
* 'bindto' => '192.168.10.1:0'
|
||||
* ),
|
||||
* 'tls' => array(
|
||||
* 'verify_peer' => false,
|
||||
* 'verify_peer_name' => false
|
||||
* )
|
||||
* ));
|
||||
*
|
||||
* $browser = new Clue\React\Buzz\Browser($loop, $connector);
|
||||
* ```
|
||||
*
|
||||
* @param LoopInterface $loop
|
||||
* @param ConnectorInterface|null $connector [optional] Connector to use.
|
||||
* Should be `null` in order to use default Connector.
|
||||
*/
|
||||
public function __construct(LoopInterface $loop, ConnectorInterface $connector = null)
|
||||
{
|
||||
$this->messageFactory = new MessageFactory();
|
||||
$this->transaction = new Transaction(
|
||||
Sender::createFromLoop($loop, $connector, $this->messageFactory),
|
||||
$this->messageFactory,
|
||||
$loop
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an HTTP GET request
|
||||
*
|
||||
* ```php
|
||||
* $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) {
|
||||
* var_dump((string)$response->getBody());
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* See also [example 01](../examples/01-google.php).
|
||||
*
|
||||
* > For BC reasons, this method accepts the `$url` as either a `string`
|
||||
* value or as an `UriInterface`. It's recommended to explicitly cast any
|
||||
* objects implementing `UriInterface` to `string`.
|
||||
*
|
||||
* @param string|UriInterface $url URL for the request.
|
||||
* @param array $headers
|
||||
* @return PromiseInterface<ResponseInterface>
|
||||
*/
|
||||
public function get($url, array $headers = array())
|
||||
{
|
||||
return $this->requestMayBeStreaming('GET', $url, $headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an HTTP POST request
|
||||
*
|
||||
* ```php
|
||||
* $browser->post(
|
||||
* $url,
|
||||
* [
|
||||
* 'Content-Type' => 'application/json'
|
||||
* ],
|
||||
* json_encode($data)
|
||||
* )->then(function (Psr\Http\Message\ResponseInterface $response) {
|
||||
* var_dump(json_decode((string)$response->getBody()));
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* See also [example 04](../examples/04-post-json.php).
|
||||
*
|
||||
* This method is also commonly used to submit HTML form data:
|
||||
*
|
||||
* ```php
|
||||
* $data = [
|
||||
* 'user' => 'Alice',
|
||||
* 'password' => 'secret'
|
||||
* ];
|
||||
*
|
||||
* $browser->post(
|
||||
* $url,
|
||||
* [
|
||||
* 'Content-Type' => 'application/x-www-form-urlencoded'
|
||||
* ],
|
||||
* http_build_query($data)
|
||||
* );
|
||||
* ```
|
||||
*
|
||||
* This method will automatically add a matching `Content-Length` request
|
||||
* header if the outgoing request body is a `string`. If you're using a
|
||||
* streaming request body (`ReadableStreamInterface`), it will default to
|
||||
* using `Transfer-Encoding: chunked` or you have to explicitly pass in a
|
||||
* matching `Content-Length` request header like so:
|
||||
*
|
||||
* ```php
|
||||
* $body = new React\Stream\ThroughStream();
|
||||
* $loop->addTimer(1.0, function () use ($body) {
|
||||
* $body->end("hello world");
|
||||
* });
|
||||
*
|
||||
* $browser->post($url, array('Content-Length' => '11'), $body);
|
||||
* ```
|
||||
*
|
||||
* > For BC reasons, this method accepts the `$url` as either a `string`
|
||||
* value or as an `UriInterface`. It's recommended to explicitly cast any
|
||||
* objects implementing `UriInterface` to `string`.
|
||||
*
|
||||
* @param string|UriInterface $url URL for the request.
|
||||
* @param array $headers
|
||||
* @param string|ReadableStreamInterface $contents
|
||||
* @return PromiseInterface<ResponseInterface>
|
||||
*/
|
||||
public function post($url, array $headers = array(), $contents = '')
|
||||
{
|
||||
return $this->requestMayBeStreaming('POST', $url, $headers, $contents);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an HTTP HEAD request
|
||||
*
|
||||
* ```php
|
||||
* $browser->head($url)->then(function (Psr\Http\Message\ResponseInterface $response) {
|
||||
* var_dump($response->getHeaders());
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* > For BC reasons, this method accepts the `$url` as either a `string`
|
||||
* value or as an `UriInterface`. It's recommended to explicitly cast any
|
||||
* objects implementing `UriInterface` to `string`.
|
||||
*
|
||||
* @param string|UriInterface $url URL for the request.
|
||||
* @param array $headers
|
||||
* @return PromiseInterface<ResponseInterface>
|
||||
*/
|
||||
public function head($url, array $headers = array())
|
||||
{
|
||||
return $this->requestMayBeStreaming('HEAD', $url, $headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an HTTP PATCH request
|
||||
*
|
||||
* ```php
|
||||
* $browser->patch(
|
||||
* $url,
|
||||
* [
|
||||
* 'Content-Type' => 'application/json'
|
||||
* ],
|
||||
* json_encode($data)
|
||||
* )->then(function (Psr\Http\Message\ResponseInterface $response) {
|
||||
* var_dump(json_decode((string)$response->getBody()));
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* This method will automatically add a matching `Content-Length` request
|
||||
* header if the outgoing request body is a `string`. If you're using a
|
||||
* streaming request body (`ReadableStreamInterface`), it will default to
|
||||
* using `Transfer-Encoding: chunked` or you have to explicitly pass in a
|
||||
* matching `Content-Length` request header like so:
|
||||
*
|
||||
* ```php
|
||||
* $body = new React\Stream\ThroughStream();
|
||||
* $loop->addTimer(1.0, function () use ($body) {
|
||||
* $body->end("hello world");
|
||||
* });
|
||||
*
|
||||
* $browser->patch($url, array('Content-Length' => '11'), $body);
|
||||
* ```
|
||||
*
|
||||
* > For BC reasons, this method accepts the `$url` as either a `string`
|
||||
* value or as an `UriInterface`. It's recommended to explicitly cast any
|
||||
* objects implementing `UriInterface` to `string`.
|
||||
*
|
||||
* @param string|UriInterface $url URL for the request.
|
||||
* @param array $headers
|
||||
* @param string|ReadableStreamInterface $contents
|
||||
* @return PromiseInterface<ResponseInterface>
|
||||
*/
|
||||
public function patch($url, array $headers = array(), $contents = '')
|
||||
{
|
||||
return $this->requestMayBeStreaming('PATCH', $url , $headers, $contents);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an HTTP PUT request
|
||||
*
|
||||
* ```php
|
||||
* $browser->put(
|
||||
* $url,
|
||||
* [
|
||||
* 'Content-Type' => 'text/xml'
|
||||
* ],
|
||||
* $xml->asXML()
|
||||
* )->then(function (Psr\Http\Message\ResponseInterface $response) {
|
||||
* var_dump((string)$response->getBody());
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* See also [example 05](../examples/05-put-xml.php).
|
||||
*
|
||||
* This method will automatically add a matching `Content-Length` request
|
||||
* header if the outgoing request body is a `string`. If you're using a
|
||||
* streaming request body (`ReadableStreamInterface`), it will default to
|
||||
* using `Transfer-Encoding: chunked` or you have to explicitly pass in a
|
||||
* matching `Content-Length` request header like so:
|
||||
*
|
||||
* ```php
|
||||
* $body = new React\Stream\ThroughStream();
|
||||
* $loop->addTimer(1.0, function () use ($body) {
|
||||
* $body->end("hello world");
|
||||
* });
|
||||
*
|
||||
* $browser->put($url, array('Content-Length' => '11'), $body);
|
||||
* ```
|
||||
*
|
||||
* > For BC reasons, this method accepts the `$url` as either a `string`
|
||||
* value or as an `UriInterface`. It's recommended to explicitly cast any
|
||||
* objects implementing `UriInterface` to `string`.
|
||||
*
|
||||
* @param string|UriInterface $url URL for the request.
|
||||
* @param array $headers
|
||||
* @param string|ReadableStreamInterface $contents
|
||||
* @return PromiseInterface<ResponseInterface>
|
||||
*/
|
||||
public function put($url, array $headers = array(), $contents = '')
|
||||
{
|
||||
return $this->requestMayBeStreaming('PUT', $url, $headers, $contents);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an HTTP DELETE request
|
||||
*
|
||||
* ```php
|
||||
* $browser->delete($url)->then(function (Psr\Http\Message\ResponseInterface $response) {
|
||||
* var_dump((string)$response->getBody());
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* > For BC reasons, this method accepts the `$url` as either a `string`
|
||||
* value or as an `UriInterface`. It's recommended to explicitly cast any
|
||||
* objects implementing `UriInterface` to `string`.
|
||||
*
|
||||
* @param string|UriInterface $url URL for the request.
|
||||
* @param array $headers
|
||||
* @param string|ReadableStreamInterface $contents
|
||||
* @return PromiseInterface<ResponseInterface>
|
||||
*/
|
||||
public function delete($url, array $headers = array(), $contents = '')
|
||||
{
|
||||
return $this->requestMayBeStreaming('DELETE', $url, $headers, $contents);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an arbitrary HTTP request.
|
||||
*
|
||||
* The preferred way to send an HTTP request is by using the above
|
||||
* [request methods](#request-methods), for example the [`get()`](#get)
|
||||
* method to send an HTTP `GET` request.
|
||||
*
|
||||
* As an alternative, if you want to use a custom HTTP request method, you
|
||||
* can use this method:
|
||||
*
|
||||
* ```php
|
||||
* $browser->request('OPTIONS', $url)->then(function (Psr\Http\Message\ResponseInterface $response) {
|
||||
* var_dump((string)$response->getBody());
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* This method will automatically add a matching `Content-Length` request
|
||||
* header if the size of the outgoing request body is known and non-empty.
|
||||
* For an empty request body, if will only include a `Content-Length: 0`
|
||||
* request header if the request method usually expects a request body (only
|
||||
* applies to `POST`, `PUT` and `PATCH`).
|
||||
*
|
||||
* If you're using a streaming request body (`ReadableStreamInterface`), it
|
||||
* will default to using `Transfer-Encoding: chunked` or you have to
|
||||
* explicitly pass in a matching `Content-Length` request header like so:
|
||||
*
|
||||
* ```php
|
||||
* $body = new React\Stream\ThroughStream();
|
||||
* $loop->addTimer(1.0, function () use ($body) {
|
||||
* $body->end("hello world");
|
||||
* });
|
||||
*
|
||||
* $browser->request('POST', $url, array('Content-Length' => '11'), $body);
|
||||
* ```
|
||||
*
|
||||
* > Note that this method is available as of v2.9.0 and always buffers the
|
||||
* response body before resolving.
|
||||
* It does not respect the deprecated [`streaming` option](#withoptions).
|
||||
* If you want to stream the response body, you can use the
|
||||
* [`requestStreaming()`](#requeststreaming) method instead.
|
||||
*
|
||||
* @param string $method HTTP request method, e.g. GET/HEAD/POST etc.
|
||||
* @param string $url URL for the request
|
||||
* @param array $headers Additional request headers
|
||||
* @param string|ReadableStreamInterface $body HTTP request body contents
|
||||
* @return PromiseInterface<ResponseInterface,Exception>
|
||||
* @since 2.9.0
|
||||
*/
|
||||
public function request($method, $url, array $headers = array(), $body = '')
|
||||
{
|
||||
return $this->withOptions(array('streaming' => false))->requestMayBeStreaming($method, $url, $headers, $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an arbitrary HTTP request and receives a streaming response without buffering the response body.
|
||||
*
|
||||
* The preferred way to send an HTTP request is by using the above
|
||||
* [request methods](#request-methods), for example the [`get()`](#get)
|
||||
* method to send an HTTP `GET` request. Each of these methods will buffer
|
||||
* the whole response body in memory by default. This is easy to get started
|
||||
* and works reasonably well for smaller responses.
|
||||
*
|
||||
* In some situations, it's a better idea to use a streaming approach, where
|
||||
* only small chunks have to be kept in memory. You can use this method to
|
||||
* send an arbitrary HTTP request and receive a streaming response. It uses
|
||||
* the same HTTP message API, but does not buffer the response body in
|
||||
* memory. It only processes the response body in small chunks as data is
|
||||
* received and forwards this data through [ReactPHP's Stream API](https://github.com/reactphp/stream).
|
||||
* This works for (any number of) responses of arbitrary sizes.
|
||||
*
|
||||
* ```php
|
||||
* $browser->requestStreaming('GET', $url)->then(function (Psr\Http\Message\ResponseInterface $response) {
|
||||
* $body = $response->getBody();
|
||||
* assert($body instanceof Psr\Http\Message\StreamInterface);
|
||||
* assert($body instanceof React\Stream\ReadableStreamInterface);
|
||||
*
|
||||
* $body->on('data', function ($chunk) {
|
||||
* echo $chunk;
|
||||
* });
|
||||
*
|
||||
* $body->on('error', function (Exception $error) {
|
||||
* echo 'Error: ' . $error->getMessage() . PHP_EOL;
|
||||
* });
|
||||
*
|
||||
* $body->on('close', function () {
|
||||
* echo '[DONE]' . PHP_EOL;
|
||||
* });
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* See also [`ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface)
|
||||
* and the [streaming response](#streaming-response) for more details,
|
||||
* examples and possible use-cases.
|
||||
*
|
||||
* This method will automatically add a matching `Content-Length` request
|
||||
* header if the size of the outgoing request body is known and non-empty.
|
||||
* For an empty request body, if will only include a `Content-Length: 0`
|
||||
* request header if the request method usually expects a request body (only
|
||||
* applies to `POST`, `PUT` and `PATCH`).
|
||||
*
|
||||
* If you're using a streaming request body (`ReadableStreamInterface`), it
|
||||
* will default to using `Transfer-Encoding: chunked` or you have to
|
||||
* explicitly pass in a matching `Content-Length` request header like so:
|
||||
*
|
||||
* ```php
|
||||
* $body = new React\Stream\ThroughStream();
|
||||
* $loop->addTimer(1.0, function () use ($body) {
|
||||
* $body->end("hello world");
|
||||
* });
|
||||
*
|
||||
* $browser->requestStreaming('POST', $url, array('Content-Length' => '11'), $body);
|
||||
* ```
|
||||
*
|
||||
* > Note that this method is available as of v2.9.0 and always resolves the
|
||||
* response without buffering the response body.
|
||||
* It does not respect the deprecated [`streaming` option](#withoptions).
|
||||
* If you want to buffer the response body, use can use the
|
||||
* [`request()`](#request) method instead.
|
||||
*
|
||||
* @param string $method HTTP request method, e.g. GET/HEAD/POST etc.
|
||||
* @param string $url URL for the request
|
||||
* @param array $headers Additional request headers
|
||||
* @param string|ReadableStreamInterface $body HTTP request body contents
|
||||
* @return PromiseInterface<ResponseInterface,Exception>
|
||||
* @since 2.9.0
|
||||
*/
|
||||
public function requestStreaming($method, $url, $headers = array(), $contents = '')
|
||||
{
|
||||
return $this->withOptions(array('streaming' => true))->requestMayBeStreaming($method, $url, $headers, $contents);
|
||||
}
|
||||
|
||||
/**
|
||||
* [Deprecated] Submits an array of field values similar to submitting a form (`application/x-www-form-urlencoded`).
|
||||
*
|
||||
* ```php
|
||||
* // deprecated: see post() instead
|
||||
* $browser->submit($url, array('user' => 'test', 'password' => 'secret'));
|
||||
* ```
|
||||
*
|
||||
* This method will automatically add a matching `Content-Length` request
|
||||
* header for the encoded length of the given `$fields`.
|
||||
*
|
||||
* > For BC reasons, this method accepts the `$url` as either a `string`
|
||||
* value or as an `UriInterface`. It's recommended to explicitly cast any
|
||||
* objects implementing `UriInterface` to `string`.
|
||||
*
|
||||
* @param string|UriInterface $url URL for the request.
|
||||
* @param array $fields
|
||||
* @param array $headers
|
||||
* @param string $method
|
||||
* @return PromiseInterface<ResponseInterface>
|
||||
* @deprecated 2.9.0 See self::post() instead.
|
||||
* @see self::post()
|
||||
*/
|
||||
public function submit($url, array $fields, $headers = array(), $method = 'POST')
|
||||
{
|
||||
$headers['Content-Type'] = 'application/x-www-form-urlencoded';
|
||||
$contents = http_build_query($fields);
|
||||
|
||||
return $this->requestMayBeStreaming($method, $url, $headers, $contents);
|
||||
}
|
||||
|
||||
/**
|
||||
* [Deprecated] Sends an arbitrary instance implementing the [`RequestInterface`](#requestinterface) (PSR-7).
|
||||
*
|
||||
* The preferred way to send an HTTP request is by using the above
|
||||
* [request methods](#request-methods), for example the [`get()`](#get)
|
||||
* method to send an HTTP `GET` request.
|
||||
*
|
||||
* As an alternative, if you want to use a custom HTTP request method, you
|
||||
* can use this method:
|
||||
*
|
||||
* ```php
|
||||
* $request = new Request('OPTIONS', $url);
|
||||
*
|
||||
* // deprecated: see request() instead
|
||||
* $browser->send($request)->then(…);
|
||||
* ```
|
||||
*
|
||||
* This method will automatically add a matching `Content-Length` request
|
||||
* header if the size of the outgoing request body is known and non-empty.
|
||||
* For an empty request body, if will only include a `Content-Length: 0`
|
||||
* request header if the request method usually expects a request body (only
|
||||
* applies to `POST`, `PUT` and `PATCH`).
|
||||
*
|
||||
* @param RequestInterface $request
|
||||
* @return PromiseInterface<ResponseInterface>
|
||||
* @deprecated 2.9.0 See self::request() instead.
|
||||
* @see self::request()
|
||||
*/
|
||||
public function send(RequestInterface $request)
|
||||
{
|
||||
if ($this->baseUrl !== null) {
|
||||
// ensure we're actually below the base URL
|
||||
$request = $request->withUri($this->messageFactory->expandBase($request->getUri(), $this->baseUrl));
|
||||
}
|
||||
|
||||
return $this->transaction->send($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the maximum timeout used for waiting for pending requests.
|
||||
*
|
||||
* You can pass in the number of seconds to use as a new timeout value:
|
||||
*
|
||||
* ```php
|
||||
* $browser = $browser->withTimeout(10.0);
|
||||
* ```
|
||||
*
|
||||
* You can pass in a bool `false` to disable any timeouts. In this case,
|
||||
* requests can stay pending forever:
|
||||
*
|
||||
* ```php
|
||||
* $browser = $browser->withTimeout(false);
|
||||
* ```
|
||||
*
|
||||
* You can pass in a bool `true` to re-enable default timeout handling. This
|
||||
* will respects PHP's `default_socket_timeout` setting (default 60s):
|
||||
*
|
||||
* ```php
|
||||
* $browser = $browser->withTimeout(true);
|
||||
* ```
|
||||
*
|
||||
* See also [timeouts](#timeouts) for more details about timeout handling.
|
||||
*
|
||||
* Notice that the [`Browser`](#browser) is an immutable object, i.e. this
|
||||
* method actually returns a *new* [`Browser`](#browser) instance with the
|
||||
* given timeout value applied.
|
||||
*
|
||||
* @param bool|number $timeout
|
||||
* @return self
|
||||
*/
|
||||
public function withTimeout($timeout)
|
||||
{
|
||||
if ($timeout === true) {
|
||||
$timeout = null;
|
||||
} elseif ($timeout === false) {
|
||||
$timeout = -1;
|
||||
} elseif ($timeout < 0) {
|
||||
$timeout = 0;
|
||||
}
|
||||
|
||||
return $this->withOptions(array(
|
||||
'timeout' => $timeout,
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes how HTTP redirects will be followed.
|
||||
*
|
||||
* You can pass in the maximum number of redirects to follow:
|
||||
*
|
||||
* ```php
|
||||
* $new = $browser->withFollowRedirects(5);
|
||||
* ```
|
||||
*
|
||||
* The request will automatically be rejected when the number of redirects
|
||||
* is exceeded. You can pass in a `0` to reject the request for any
|
||||
* redirects encountered:
|
||||
*
|
||||
* ```php
|
||||
* $browser = $browser->withFollowRedirects(0);
|
||||
*
|
||||
* $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) {
|
||||
* // only non-redirected responses will now end up here
|
||||
* var_dump($response->getHeaders());
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* You can pass in a bool `false` to disable following any redirects. In
|
||||
* this case, requests will resolve with the redirection response instead
|
||||
* of following the `Location` response header:
|
||||
*
|
||||
* ```php
|
||||
* $browser = $browser->withFollowRedirects(false);
|
||||
*
|
||||
* $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) {
|
||||
* // any redirects will now end up here
|
||||
* var_dump($response->getHeaderLine('Location'));
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* You can pass in a bool `true` to re-enable default redirect handling.
|
||||
* This defaults to following a maximum of 10 redirects:
|
||||
*
|
||||
* ```php
|
||||
* $browser = $browser->withFollowRedirects(true);
|
||||
* ```
|
||||
*
|
||||
* See also [redirects](#redirects) for more details about redirect handling.
|
||||
*
|
||||
* Notice that the [`Browser`](#browser) is an immutable object, i.e. this
|
||||
* method actually returns a *new* [`Browser`](#browser) instance with the
|
||||
* given redirect setting applied.
|
||||
*
|
||||
* @param bool|int $followRedirects
|
||||
* @return self
|
||||
*/
|
||||
public function withFollowRedirects($followRedirects)
|
||||
{
|
||||
return $this->withOptions(array(
|
||||
'followRedirects' => $followRedirects !== false,
|
||||
'maxRedirects' => \is_bool($followRedirects) ? null : $followRedirects
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes whether non-successful HTTP response status codes (4xx and 5xx) will be rejected.
|
||||
*
|
||||
* You can pass in a bool `false` to disable rejecting incoming responses
|
||||
* that use a 4xx or 5xx response status code. In this case, requests will
|
||||
* resolve with the response message indicating an error condition:
|
||||
*
|
||||
* ```php
|
||||
* $browser = $browser->withRejectErrorResponse(false);
|
||||
*
|
||||
* $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) {
|
||||
* // any HTTP response will now end up here
|
||||
* var_dump($response->getStatusCode(), $response->getReasonPhrase());
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* You can pass in a bool `true` to re-enable default status code handling.
|
||||
* This defaults to rejecting any response status codes in the 4xx or 5xx
|
||||
* range:
|
||||
*
|
||||
* ```php
|
||||
* $browser = $browser->withRejectErrorResponse(true);
|
||||
*
|
||||
* $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) {
|
||||
* // any successful HTTP response will now end up here
|
||||
* var_dump($response->getStatusCode(), $response->getReasonPhrase());
|
||||
* }, function (Exception $e) {
|
||||
* if ($e instanceof Clue\React\Buzz\Message\ResponseException) {
|
||||
* // any HTTP response error message will now end up here
|
||||
* $response = $e->getResponse();
|
||||
* var_dump($response->getStatusCode(), $response->getReasonPhrase());
|
||||
* } else {
|
||||
* var_dump($e->getMessage());
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* Notice that the [`Browser`](#browser) is an immutable object, i.e. this
|
||||
* method actually returns a *new* [`Browser`](#browser) instance with the
|
||||
* given setting applied.
|
||||
*
|
||||
* @param bool $obeySuccessCode
|
||||
* @return self
|
||||
*/
|
||||
public function withRejectErrorResponse($obeySuccessCode)
|
||||
{
|
||||
return $this->withOptions(array(
|
||||
'obeySuccessCode' => $obeySuccessCode,
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the base URL used to resolve relative URLs to.
|
||||
*
|
||||
* If you configure a base URL, any requests to relative URLs will be
|
||||
* processed by first prepending this absolute base URL. Note that this
|
||||
* merely prepends the base URL and does *not* resolve any relative path
|
||||
* references (like `../` etc.). This is mostly useful for (RESTful) API
|
||||
* calls where all endpoints (URLs) are located under a common base URL.
|
||||
*
|
||||
* ```php
|
||||
* $browser = $browser->withBase('http://api.example.com/v3');
|
||||
*
|
||||
* // will request http://api.example.com/v3/example
|
||||
* $browser->get('/example')->then(…);
|
||||
* ```
|
||||
*
|
||||
* You can pass in a `null` base URL to return a new instance that does not
|
||||
* use a base URL:
|
||||
*
|
||||
* ```php
|
||||
* $browser = $browser->withBase(null);
|
||||
* ```
|
||||
*
|
||||
* Accordingly, any requests using relative URLs to a browser that does not
|
||||
* use a base URL can not be completed and will be rejected without sending
|
||||
* a request.
|
||||
*
|
||||
* This method will throw an `InvalidArgumentException` if the given
|
||||
* `$baseUrl` argument is not a valid URL.
|
||||
*
|
||||
* Notice that the [`Browser`](#browser) is an immutable object, i.e. the `withBase()` method
|
||||
* actually returns a *new* [`Browser`](#browser) instance with the given base URL applied.
|
||||
*
|
||||
* > For BC reasons, this method accepts the `$baseUrl` as either a `string`
|
||||
* value or as an `UriInterface`. It's recommended to explicitly cast any
|
||||
* objects implementing `UriInterface` to `string`.
|
||||
*
|
||||
* > Changelog: As of v2.9.0 this method accepts a `null` value to reset the
|
||||
* base URL. Earlier versions had to use the deprecated `withoutBase()`
|
||||
* method to reset the base URL.
|
||||
*
|
||||
* @param string|null|UriInterface $baseUrl absolute base URL
|
||||
* @return self
|
||||
* @throws InvalidArgumentException if the given $baseUrl is not a valid absolute URL
|
||||
* @see self::withoutBase()
|
||||
*/
|
||||
public function withBase($baseUrl)
|
||||
{
|
||||
$browser = clone $this;
|
||||
if ($baseUrl === null) {
|
||||
$browser->baseUrl = null;
|
||||
return $browser;
|
||||
}
|
||||
|
||||
$browser->baseUrl = $this->messageFactory->uri($baseUrl);
|
||||
if (!\in_array($browser->baseUrl->getScheme(), array('http', 'https')) || $browser->baseUrl->getHost() === '') {
|
||||
throw new \InvalidArgumentException('Base URL must be absolute');
|
||||
}
|
||||
|
||||
return $browser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the HTTP protocol version that will be used for all subsequent requests.
|
||||
*
|
||||
* All the above [request methods](#request-methods) default to sending
|
||||
* requests as HTTP/1.1. This is the preferred HTTP protocol version which
|
||||
* also provides decent backwards-compatibility with legacy HTTP/1.0
|
||||
* servers. As such, there should rarely be a need to explicitly change this
|
||||
* protocol version.
|
||||
*
|
||||
* If you want to explicitly use the legacy HTTP/1.0 protocol version, you
|
||||
* can use this method:
|
||||
*
|
||||
* ```php
|
||||
* $newBrowser = $browser->withProtocolVersion('1.0');
|
||||
*
|
||||
* $newBrowser->get($url)->then(…);
|
||||
* ```
|
||||
*
|
||||
* Notice that the [`Browser`](#browser) is an immutable object, i.e. this
|
||||
* method actually returns a *new* [`Browser`](#browser) instance with the
|
||||
* new protocol version applied.
|
||||
*
|
||||
* @param string $protocolVersion HTTP protocol version to use, must be one of "1.1" or "1.0"
|
||||
* @return self
|
||||
* @throws InvalidArgumentException
|
||||
* @since 2.8.0
|
||||
*/
|
||||
public function withProtocolVersion($protocolVersion)
|
||||
{
|
||||
if (!\in_array($protocolVersion, array('1.0', '1.1'), true)) {
|
||||
throw new InvalidArgumentException('Invalid HTTP protocol version, must be one of "1.1" or "1.0"');
|
||||
}
|
||||
|
||||
$browser = clone $this;
|
||||
$browser->protocolVersion = (string) $protocolVersion;
|
||||
|
||||
return $browser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the maximum size for buffering a response body.
|
||||
*
|
||||
* The preferred way to send an HTTP request is by using the above
|
||||
* [request methods](#request-methods), for example the [`get()`](#get)
|
||||
* method to send an HTTP `GET` request. Each of these methods will buffer
|
||||
* the whole response body in memory by default. This is easy to get started
|
||||
* and works reasonably well for smaller responses.
|
||||
*
|
||||
* By default, the response body buffer will be limited to 16 MiB. If the
|
||||
* response body exceeds this maximum size, the request will be rejected.
|
||||
*
|
||||
* You can pass in the maximum number of bytes to buffer:
|
||||
*
|
||||
* ```php
|
||||
* $browser = $browser->withResponseBuffer(1024 * 1024);
|
||||
*
|
||||
* $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) {
|
||||
* // response body will not exceed 1 MiB
|
||||
* var_dump($response->getHeaders(), (string) $response->getBody());
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* Note that the response body buffer has to be kept in memory for each
|
||||
* pending request until its transfer is completed and it will only be freed
|
||||
* after a pending request is fulfilled. As such, increasing this maximum
|
||||
* buffer size to allow larger response bodies is usually not recommended.
|
||||
* Instead, you can use the [`requestStreaming()` method](#requeststreaming)
|
||||
* to receive responses with arbitrary sizes without buffering. Accordingly,
|
||||
* this maximum buffer size setting has no effect on streaming responses.
|
||||
*
|
||||
* Notice that the [`Browser`](#browser) is an immutable object, i.e. this
|
||||
* method actually returns a *new* [`Browser`](#browser) instance with the
|
||||
* given setting applied.
|
||||
*
|
||||
* @param int $maximumSize
|
||||
* @return self
|
||||
* @see self::requestStreaming()
|
||||
*/
|
||||
public function withResponseBuffer($maximumSize)
|
||||
{
|
||||
return $this->withOptions(array(
|
||||
'maximumSize' => $maximumSize
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* [Deprecated] Changes the [options](#options) to use:
|
||||
*
|
||||
* The [`Browser`](#browser) class exposes several options for the handling of
|
||||
* HTTP transactions. These options resemble some of PHP's
|
||||
* [HTTP context options](http://php.net/manual/en/context.http.php) and
|
||||
* can be controlled via the following API (and their defaults):
|
||||
*
|
||||
* ```php
|
||||
* // deprecated
|
||||
* $newBrowser = $browser->withOptions(array(
|
||||
* 'timeout' => null, // see withTimeout() instead
|
||||
* 'followRedirects' => true, // see withFollowRedirects() instead
|
||||
* 'maxRedirects' => 10, // see withFollowRedirects() instead
|
||||
* 'obeySuccessCode' => true, // see withRejectErrorResponse() instead
|
||||
* 'streaming' => false, // deprecated, see requestStreaming() instead
|
||||
* ));
|
||||
* ```
|
||||
*
|
||||
* See also [timeouts](#timeouts), [redirects](#redirects) and
|
||||
* [streaming](#streaming) for more details.
|
||||
*
|
||||
* Notice that the [`Browser`](#browser) is an immutable object, i.e. this
|
||||
* method actually returns a *new* [`Browser`](#browser) instance with the
|
||||
* options applied.
|
||||
*
|
||||
* @param array $options
|
||||
* @return self
|
||||
* @deprecated 2.9.0 See self::withTimeout(), self::withFollowRedirects() and self::withRejectErrorResponse() instead.
|
||||
* @see self::withTimeout()
|
||||
* @see self::withFollowRedirects()
|
||||
* @see self::withRejectErrorResponse()
|
||||
*/
|
||||
public function withOptions(array $options)
|
||||
{
|
||||
$browser = clone $this;
|
||||
$browser->transaction = $this->transaction->withOptions($options);
|
||||
|
||||
return $browser;
|
||||
}
|
||||
|
||||
/**
|
||||
* [Deprecated] Removes the base URL.
|
||||
*
|
||||
* ```php
|
||||
* // deprecated: see withBase() instead
|
||||
* $newBrowser = $browser->withoutBase();
|
||||
* ```
|
||||
*
|
||||
* Notice that the [`Browser`](#browser) is an immutable object, i.e. the `withoutBase()` method
|
||||
* actually returns a *new* [`Browser`](#browser) instance without any base URL applied.
|
||||
*
|
||||
* See also [`withBase()`](#withbase).
|
||||
*
|
||||
* @return self
|
||||
* @deprecated 2.9.0 See self::withBase() instead.
|
||||
* @see self::withBase()
|
||||
*/
|
||||
public function withoutBase()
|
||||
{
|
||||
return $this->withBase(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $method
|
||||
* @param string|UriInterface $url
|
||||
* @param array $headers
|
||||
* @param string|ReadableStreamInterface $contents
|
||||
* @return PromiseInterface<ResponseInterface,Exception>
|
||||
*/
|
||||
private function requestMayBeStreaming($method, $url, array $headers = array(), $contents = '')
|
||||
{
|
||||
return $this->send($this->messageFactory->request($method, $url, $headers, $contents, $this->protocolVersion));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
<?php
|
||||
|
||||
namespace Clue\React\Buzz\Io;
|
||||
|
||||
use Evenement\EventEmitter;
|
||||
use React\Stream\ReadableStreamInterface;
|
||||
use React\Stream\Util;
|
||||
use React\Stream\WritableStreamInterface;
|
||||
|
||||
/**
|
||||
* [Internal] Encodes given payload stream with "Transfer-Encoding: chunked" and emits encoded data
|
||||
*
|
||||
* This is used internally to encode outgoing requests with this encoding.
|
||||
*
|
||||
* @internal
|
||||
* @link https://github.com/reactphp/http/blob/master/src/Io/ChunkedEncoder.php Originally from react/http
|
||||
*/
|
||||
class ChunkedEncoder extends EventEmitter implements ReadableStreamInterface
|
||||
{
|
||||
private $input;
|
||||
private $closed = false;
|
||||
|
||||
public function __construct(ReadableStreamInterface $input)
|
||||
{
|
||||
$this->input = $input;
|
||||
|
||||
$this->input->on('data', array($this, 'handleData'));
|
||||
$this->input->on('end', array($this, 'handleEnd'));
|
||||
$this->input->on('error', array($this, 'handleError'));
|
||||
$this->input->on('close', array($this, 'close'));
|
||||
}
|
||||
|
||||
public function isReadable()
|
||||
{
|
||||
return !$this->closed && $this->input->isReadable();
|
||||
}
|
||||
|
||||
public function pause()
|
||||
{
|
||||
$this->input->pause();
|
||||
}
|
||||
|
||||
public function resume()
|
||||
{
|
||||
$this->input->resume();
|
||||
}
|
||||
|
||||
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->input->close();
|
||||
|
||||
$this->emit('close');
|
||||
$this->removeAllListeners();
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
public function handleData($data)
|
||||
{
|
||||
if ($data !== '') {
|
||||
$this->emit('data', array(
|
||||
dechex(strlen($data)) . "\r\n" . $data . "\r\n"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
public function handleError(\Exception $e)
|
||||
{
|
||||
$this->emit('error', array($e));
|
||||
$this->close();
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
public function handleEnd()
|
||||
{
|
||||
$this->emit('data', array("0\r\n\r\n"));
|
||||
|
||||
if (!$this->closed) {
|
||||
$this->emit('end');
|
||||
$this->close();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,161 @@
|
|||
<?php
|
||||
|
||||
namespace Clue\React\Buzz\Io;
|
||||
|
||||
use Clue\React\Buzz\Message\MessageFactory;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use React\EventLoop\LoopInterface;
|
||||
use React\HttpClient\Client as HttpClient;
|
||||
use React\HttpClient\Response as ResponseStream;
|
||||
use React\Promise\PromiseInterface;
|
||||
use React\Promise\Deferred;
|
||||
use React\Socket\ConnectorInterface;
|
||||
use React\Stream\ReadableStreamInterface;
|
||||
|
||||
/**
|
||||
* [Internal] Sends requests and receives responses
|
||||
*
|
||||
* The `Sender` is responsible for passing the [`RequestInterface`](#requestinterface) objects to
|
||||
* the underlying [`HttpClient`](https://github.com/reactphp/http-client) library
|
||||
* and keeps track of its transmission and converts its reponses back to [`ResponseInterface`](#responseinterface) objects.
|
||||
*
|
||||
* It also registers everything with the main [`EventLoop`](https://github.com/reactphp/event-loop#usage)
|
||||
* and the default [`Connector`](https://github.com/reactphp/socket-client) and [DNS `Resolver`](https://github.com/reactphp/dns).
|
||||
*
|
||||
* The `Sender` class mostly exists in order to abstract changes on the underlying
|
||||
* components away from this package in order to provide backwards and forwards
|
||||
* compatibility.
|
||||
*
|
||||
* @internal You SHOULD NOT rely on this API, it is subject to change without prior notice!
|
||||
* @see Browser
|
||||
*/
|
||||
class Sender
|
||||
{
|
||||
/**
|
||||
* create a new default sender attached to the given event loop
|
||||
*
|
||||
* This method is used internally to create the "default sender".
|
||||
*
|
||||
* You may also use this method if you need custom DNS or connector
|
||||
* settings. You can use this method manually like this:
|
||||
*
|
||||
* ```php
|
||||
* $connector = new \React\Socket\Connector($loop);
|
||||
* $sender = \Clue\React\Buzz\Io\Sender::createFromLoop($loop, $connector);
|
||||
* ```
|
||||
*
|
||||
* @param LoopInterface $loop
|
||||
* @param ConnectorInterface|null $connector
|
||||
* @return self
|
||||
*/
|
||||
public static function createFromLoop(LoopInterface $loop, ConnectorInterface $connector = null, MessageFactory $messageFactory)
|
||||
{
|
||||
return new self(new HttpClient($loop, $connector), $messageFactory);
|
||||
}
|
||||
|
||||
private $http;
|
||||
private $messageFactory;
|
||||
|
||||
/**
|
||||
* [internal] Instantiate Sender
|
||||
*
|
||||
* @param HttpClient $http
|
||||
* @internal
|
||||
*/
|
||||
public function __construct(HttpClient $http, MessageFactory $messageFactory)
|
||||
{
|
||||
$this->http = $http;
|
||||
$this->messageFactory = $messageFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @internal
|
||||
* @param RequestInterface $request
|
||||
* @return PromiseInterface Promise<ResponseInterface, Exception>
|
||||
*/
|
||||
public function send(RequestInterface $request)
|
||||
{
|
||||
$body = $request->getBody();
|
||||
$size = $body->getSize();
|
||||
|
||||
if ($size !== null && $size !== 0) {
|
||||
// automatically assign a "Content-Length" request header if the body size is known and non-empty
|
||||
$request = $request->withHeader('Content-Length', (string)$size);
|
||||
} elseif ($size === 0 && \in_array($request->getMethod(), array('POST', 'PUT', 'PATCH'))) {
|
||||
// only assign a "Content-Length: 0" request header if the body is expected for certain methods
|
||||
$request = $request->withHeader('Content-Length', '0');
|
||||
} elseif ($body instanceof ReadableStreamInterface && $body->isReadable() && !$request->hasHeader('Content-Length')) {
|
||||
// use "Transfer-Encoding: chunked" when this is a streaming body and body size is unknown
|
||||
$request = $request->withHeader('Transfer-Encoding', 'chunked');
|
||||
} else {
|
||||
// do not use chunked encoding if size is known or if this is an empty request body
|
||||
$size = 0;
|
||||
}
|
||||
|
||||
$headers = array();
|
||||
foreach ($request->getHeaders() as $name => $values) {
|
||||
$headers[$name] = implode(', ', $values);
|
||||
}
|
||||
|
||||
$requestStream = $this->http->request($request->getMethod(), (string)$request->getUri(), $headers, $request->getProtocolVersion());
|
||||
|
||||
$deferred = new Deferred(function ($_, $reject) use ($requestStream) {
|
||||
// close request stream if request is cancelled
|
||||
$reject(new \RuntimeException('Request cancelled'));
|
||||
$requestStream->close();
|
||||
});
|
||||
|
||||
$requestStream->on('error', function($error) use ($deferred) {
|
||||
$deferred->reject($error);
|
||||
});
|
||||
|
||||
$messageFactory = $this->messageFactory;
|
||||
$requestStream->on('response', function (ResponseStream $responseStream) use ($deferred, $messageFactory, $request) {
|
||||
// apply response header values from response stream
|
||||
$deferred->resolve($messageFactory->response(
|
||||
$responseStream->getVersion(),
|
||||
$responseStream->getCode(),
|
||||
$responseStream->getReasonPhrase(),
|
||||
$responseStream->getHeaders(),
|
||||
$responseStream,
|
||||
$request->getMethod()
|
||||
));
|
||||
});
|
||||
|
||||
if ($body instanceof ReadableStreamInterface) {
|
||||
if ($body->isReadable()) {
|
||||
// length unknown => apply chunked transfer-encoding
|
||||
if ($size === null) {
|
||||
$body = new ChunkedEncoder($body);
|
||||
}
|
||||
|
||||
// pipe body into request stream
|
||||
// add dummy write to immediately start request even if body does not emit any data yet
|
||||
$body->pipe($requestStream);
|
||||
$requestStream->write('');
|
||||
|
||||
$body->on('close', $close = function () use ($deferred, $requestStream) {
|
||||
$deferred->reject(new \RuntimeException('Request failed because request body closed unexpectedly'));
|
||||
$requestStream->close();
|
||||
});
|
||||
$body->on('error', function ($e) use ($deferred, $requestStream, $close, $body) {
|
||||
$body->removeListener('close', $close);
|
||||
$deferred->reject(new \RuntimeException('Request failed because request body reported an error', 0, $e));
|
||||
$requestStream->close();
|
||||
});
|
||||
$body->on('end', function () use ($close, $body) {
|
||||
$body->removeListener('close', $close);
|
||||
});
|
||||
} else {
|
||||
// stream is not readable => end request without body
|
||||
$requestStream->end();
|
||||
}
|
||||
} else {
|
||||
// body is fully buffered => write as one chunk
|
||||
$requestStream->end((string)$body);
|
||||
}
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,305 @@
|
|||
<?php
|
||||
|
||||
namespace Clue\React\Buzz\Io;
|
||||
|
||||
use Clue\React\Buzz\Message\ResponseException;
|
||||
use Clue\React\Buzz\Message\MessageFactory;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
use React\EventLoop\LoopInterface;
|
||||
use React\Promise\Deferred;
|
||||
use React\Promise\PromiseInterface;
|
||||
use React\Stream\ReadableStreamInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class Transaction
|
||||
{
|
||||
private $sender;
|
||||
private $messageFactory;
|
||||
private $loop;
|
||||
|
||||
// context: http.timeout (ini_get('default_socket_timeout'): 60)
|
||||
private $timeout;
|
||||
|
||||
// context: http.follow_location (true)
|
||||
private $followRedirects = true;
|
||||
|
||||
// context: http.max_redirects (10)
|
||||
private $maxRedirects = 10;
|
||||
|
||||
// context: http.ignore_errors (false)
|
||||
private $obeySuccessCode = true;
|
||||
|
||||
private $streaming = false;
|
||||
|
||||
private $maximumSize = 16777216; // 16 MiB = 2^24 bytes
|
||||
|
||||
public function __construct(Sender $sender, MessageFactory $messageFactory, LoopInterface $loop)
|
||||
{
|
||||
$this->sender = $sender;
|
||||
$this->messageFactory = $messageFactory;
|
||||
$this->loop = $loop;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $options
|
||||
* @return self returns new instance, without modifying existing instance
|
||||
*/
|
||||
public function withOptions(array $options)
|
||||
{
|
||||
$transaction = clone $this;
|
||||
foreach ($options as $name => $value) {
|
||||
if (property_exists($transaction, $name)) {
|
||||
// restore default value if null is given
|
||||
if ($value === null) {
|
||||
$default = new self($this->sender, $this->messageFactory, $this->loop);
|
||||
$value = $default->$name;
|
||||
}
|
||||
|
||||
$transaction->$name = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $transaction;
|
||||
}
|
||||
|
||||
public function send(RequestInterface $request)
|
||||
{
|
||||
$deferred = new Deferred(function () use (&$deferred) {
|
||||
if (isset($deferred->pending)) {
|
||||
$deferred->pending->cancel();
|
||||
unset($deferred->pending);
|
||||
}
|
||||
});
|
||||
|
||||
$deferred->numRequests = 0;
|
||||
|
||||
// use timeout from options or default to PHP's default_socket_timeout (60)
|
||||
$timeout = (float)($this->timeout !== null ? $this->timeout : ini_get("default_socket_timeout"));
|
||||
|
||||
$loop = $this->loop;
|
||||
$this->next($request, $deferred)->then(
|
||||
function (ResponseInterface $response) use ($deferred, $loop, &$timeout) {
|
||||
if (isset($deferred->timeout)) {
|
||||
$loop->cancelTimer($deferred->timeout);
|
||||
unset($deferred->timeout);
|
||||
}
|
||||
$timeout = -1;
|
||||
$deferred->resolve($response);
|
||||
},
|
||||
function ($e) use ($deferred, $loop, &$timeout) {
|
||||
if (isset($deferred->timeout)) {
|
||||
$loop->cancelTimer($deferred->timeout);
|
||||
unset($deferred->timeout);
|
||||
}
|
||||
$timeout = -1;
|
||||
$deferred->reject($e);
|
||||
}
|
||||
);
|
||||
|
||||
if ($timeout < 0) {
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
$body = $request->getBody();
|
||||
if ($body instanceof ReadableStreamInterface && $body->isReadable()) {
|
||||
$that = $this;
|
||||
$body->on('close', function () use ($that, $deferred, &$timeout) {
|
||||
if ($timeout >= 0) {
|
||||
$that->applyTimeout($deferred, $timeout);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
$this->applyTimeout($deferred, $timeout);
|
||||
}
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @param Deferred $deferred
|
||||
* @param number $timeout
|
||||
* @return void
|
||||
*/
|
||||
public function applyTimeout(Deferred $deferred, $timeout)
|
||||
{
|
||||
$deferred->timeout = $this->loop->addTimer($timeout, function () use ($timeout, $deferred) {
|
||||
$deferred->reject(new \RuntimeException(
|
||||
'Request timed out after ' . $timeout . ' seconds'
|
||||
));
|
||||
if (isset($deferred->pending)) {
|
||||
$deferred->pending->cancel();
|
||||
unset($deferred->pending);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private function next(RequestInterface $request, Deferred $deferred)
|
||||
{
|
||||
$this->progress('request', array($request));
|
||||
|
||||
$that = $this;
|
||||
++$deferred->numRequests;
|
||||
|
||||
$promise = $this->sender->send($request);
|
||||
|
||||
if (!$this->streaming) {
|
||||
$promise = $promise->then(function ($response) use ($deferred, $that) {
|
||||
return $that->bufferResponse($response, $deferred);
|
||||
});
|
||||
}
|
||||
|
||||
$deferred->pending = $promise;
|
||||
|
||||
return $promise->then(
|
||||
function (ResponseInterface $response) use ($request, $that, $deferred) {
|
||||
return $that->onResponse($response, $request, $deferred);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @param ResponseInterface $response
|
||||
* @return PromiseInterface Promise<ResponseInterface, Exception>
|
||||
*/
|
||||
public function bufferResponse(ResponseInterface $response, $deferred)
|
||||
{
|
||||
$stream = $response->getBody();
|
||||
|
||||
$size = $stream->getSize();
|
||||
if ($size !== null && $size > $this->maximumSize) {
|
||||
$stream->close();
|
||||
return \React\Promise\reject(new \OverflowException(
|
||||
'Response body size of ' . $size . ' bytes exceeds maximum of ' . $this->maximumSize . ' bytes',
|
||||
\defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 0
|
||||
));
|
||||
}
|
||||
|
||||
// body is not streaming => already buffered
|
||||
if (!$stream instanceof ReadableStreamInterface) {
|
||||
return \React\Promise\resolve($response);
|
||||
}
|
||||
|
||||
// buffer stream and resolve with buffered body
|
||||
$messageFactory = $this->messageFactory;
|
||||
$maximumSize = $this->maximumSize;
|
||||
$promise = \React\Promise\Stream\buffer($stream, $maximumSize)->then(
|
||||
function ($body) use ($response, $messageFactory) {
|
||||
return $response->withBody($messageFactory->body($body));
|
||||
},
|
||||
function ($e) use ($stream, $maximumSize) {
|
||||
// try to close stream if buffering fails (or is cancelled)
|
||||
$stream->close();
|
||||
|
||||
if ($e instanceof \OverflowException) {
|
||||
$e = new \OverflowException(
|
||||
'Response body size exceeds maximum of ' . $maximumSize . ' bytes',
|
||||
\defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 0
|
||||
);
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
);
|
||||
|
||||
$deferred->pending = $promise;
|
||||
|
||||
return $promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @param ResponseInterface $response
|
||||
* @param RequestInterface $request
|
||||
* @throws ResponseException
|
||||
* @return ResponseInterface|PromiseInterface
|
||||
*/
|
||||
public function onResponse(ResponseInterface $response, RequestInterface $request, $deferred)
|
||||
{
|
||||
$this->progress('response', array($response, $request));
|
||||
|
||||
// follow 3xx (Redirection) response status codes if Location header is present and not explicitly disabled
|
||||
// @link https://tools.ietf.org/html/rfc7231#section-6.4
|
||||
if ($this->followRedirects && ($response->getStatusCode() >= 300 && $response->getStatusCode() < 400) && $response->hasHeader('Location')) {
|
||||
return $this->onResponseRedirect($response, $request, $deferred);
|
||||
}
|
||||
|
||||
// only status codes 200-399 are considered to be valid, reject otherwise
|
||||
if ($this->obeySuccessCode && ($response->getStatusCode() < 200 || $response->getStatusCode() >= 400)) {
|
||||
throw new ResponseException($response);
|
||||
}
|
||||
|
||||
// resolve our initial promise
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ResponseInterface $response
|
||||
* @param RequestInterface $request
|
||||
* @return PromiseInterface
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
private function onResponseRedirect(ResponseInterface $response, RequestInterface $request, Deferred $deferred)
|
||||
{
|
||||
// resolve location relative to last request URI
|
||||
$location = $this->messageFactory->uriRelative($request->getUri(), $response->getHeaderLine('Location'));
|
||||
|
||||
$request = $this->makeRedirectRequest($request, $location);
|
||||
$this->progress('redirect', array($request));
|
||||
|
||||
if ($deferred->numRequests >= $this->maxRedirects) {
|
||||
throw new \RuntimeException('Maximum number of redirects (' . $this->maxRedirects . ') exceeded');
|
||||
}
|
||||
|
||||
return $this->next($request, $deferred);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param RequestInterface $request
|
||||
* @param UriInterface $location
|
||||
* @return RequestInterface
|
||||
*/
|
||||
private function makeRedirectRequest(RequestInterface $request, UriInterface $location)
|
||||
{
|
||||
$originalHost = $request->getUri()->getHost();
|
||||
$request = $request
|
||||
->withoutHeader('Host')
|
||||
->withoutHeader('Content-Type')
|
||||
->withoutHeader('Content-Length');
|
||||
|
||||
// Remove authorization if changing hostnames (but not if just changing ports or protocols).
|
||||
if ($location->getHost() !== $originalHost) {
|
||||
$request = $request->withoutHeader('Authorization');
|
||||
}
|
||||
|
||||
// naïve approach..
|
||||
$method = ($request->getMethod() === 'HEAD') ? 'HEAD' : 'GET';
|
||||
|
||||
return $this->messageFactory->request($method, $location, $request->getHeaders());
|
||||
}
|
||||
|
||||
private function progress($name, array $args = array())
|
||||
{
|
||||
return;
|
||||
|
||||
echo $name;
|
||||
|
||||
foreach ($args as $arg) {
|
||||
echo ' ';
|
||||
if ($arg instanceof ResponseInterface) {
|
||||
echo 'HTTP/' . $arg->getProtocolVersion() . ' ' . $arg->getStatusCode() . ' ' . $arg->getReasonPhrase();
|
||||
} elseif ($arg instanceof RequestInterface) {
|
||||
echo $arg->getMethod() . ' ' . $arg->getRequestTarget() . ' HTTP/' . $arg->getProtocolVersion();
|
||||
} else {
|
||||
echo $arg;
|
||||
}
|
||||
}
|
||||
|
||||
echo PHP_EOL;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
<?php
|
||||
|
||||
namespace Clue\React\Buzz\Message;
|
||||
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
use RingCentral\Psr7\Request;
|
||||
use RingCentral\Psr7\Response;
|
||||
use RingCentral\Psr7\Uri;
|
||||
use React\Stream\ReadableStreamInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class MessageFactory
|
||||
{
|
||||
/**
|
||||
* Creates a new instance of RequestInterface for the given request parameters
|
||||
*
|
||||
* @param string $method
|
||||
* @param string|UriInterface $uri
|
||||
* @param array $headers
|
||||
* @param string|ReadableStreamInterface $content
|
||||
* @param string $protocolVersion
|
||||
* @return Request
|
||||
*/
|
||||
public function request($method, $uri, $headers = array(), $content = '', $protocolVersion = '1.1')
|
||||
{
|
||||
return new Request($method, $uri, $headers, $this->body($content), $protocolVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance of ResponseInterface for the given response parameters
|
||||
*
|
||||
* @param string $protocolVersion
|
||||
* @param int $status
|
||||
* @param string $reason
|
||||
* @param array $headers
|
||||
* @param ReadableStreamInterface|string $body
|
||||
* @param ?string $requestMethod
|
||||
* @return Response
|
||||
* @uses self::body()
|
||||
*/
|
||||
public function response($protocolVersion, $status, $reason, $headers = array(), $body = '', $requestMethod = null)
|
||||
{
|
||||
$response = new Response($status, $headers, $body instanceof ReadableStreamInterface ? null : $body, $protocolVersion, $reason);
|
||||
|
||||
if ($body instanceof ReadableStreamInterface) {
|
||||
$length = null;
|
||||
$code = $response->getStatusCode();
|
||||
if ($requestMethod === 'HEAD' || ($code >= 100 && $code < 200) || $code == 204 || $code == 304) {
|
||||
$length = 0;
|
||||
} elseif (\strtolower($response->getHeaderLine('Transfer-Encoding')) === 'chunked') {
|
||||
$length = null;
|
||||
} elseif ($response->hasHeader('Content-Length')) {
|
||||
$length = (int)$response->getHeaderLine('Content-Length');
|
||||
}
|
||||
|
||||
$response = $response->withBody(new ReadableBodyStream($body, $length));
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance of StreamInterface for the given body contents
|
||||
*
|
||||
* @param ReadableStreamInterface|string $body
|
||||
* @return StreamInterface
|
||||
*/
|
||||
public function body($body)
|
||||
{
|
||||
if ($body instanceof ReadableStreamInterface) {
|
||||
return new ReadableBodyStream($body);
|
||||
}
|
||||
|
||||
return \RingCentral\Psr7\stream_for($body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance of UriInterface for the given URI string
|
||||
*
|
||||
* @param string $uri
|
||||
* @return UriInterface
|
||||
*/
|
||||
public function uri($uri)
|
||||
{
|
||||
return new Uri($uri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance of UriInterface for the given URI string relative to the given base URI
|
||||
*
|
||||
* @param UriInterface $base
|
||||
* @param string $uri
|
||||
* @return UriInterface
|
||||
*/
|
||||
public function uriRelative(UriInterface $base, $uri)
|
||||
{
|
||||
return Uri::resolve($base, $uri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the given relative or absolute $uri by appending it behind $this base URI
|
||||
*
|
||||
* The given $uri parameter can be either a relative or absolute URI and
|
||||
* as such can not contain any URI template placeholders.
|
||||
*
|
||||
* As such, the outcome of this method represents a valid, absolute URI
|
||||
* which will be returned as an instance implementing `UriInterface`.
|
||||
*
|
||||
* If the given $uri is a relative URI, it will simply be appended behind $base URI.
|
||||
*
|
||||
* If the given $uri is an absolute URI, it will simply be returned as-is.
|
||||
*
|
||||
* @param UriInterface $uri
|
||||
* @param UriInterface $base
|
||||
* @return UriInterface
|
||||
*/
|
||||
public function expandBase(UriInterface $uri, UriInterface $base)
|
||||
{
|
||||
if ($uri->getScheme() !== '') {
|
||||
return $uri;
|
||||
}
|
||||
|
||||
$uri = (string)$uri;
|
||||
$base = (string)$base;
|
||||
|
||||
if ($uri !== '' && substr($base, -1) !== '/' && substr($uri, 0, 1) !== '?') {
|
||||
$base .= '/';
|
||||
}
|
||||
|
||||
if (isset($uri[0]) && $uri[0] === '/') {
|
||||
$uri = substr($uri, 1);
|
||||
}
|
||||
|
||||
return $this->uri($base . $uri);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,153 @@
|
|||
<?php
|
||||
|
||||
namespace Clue\React\Buzz\Message;
|
||||
|
||||
use Evenement\EventEmitter;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
use React\Stream\ReadableStreamInterface;
|
||||
use React\Stream\Util;
|
||||
use React\Stream\WritableStreamInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class ReadableBodyStream extends EventEmitter implements ReadableStreamInterface, StreamInterface
|
||||
{
|
||||
private $input;
|
||||
private $position = 0;
|
||||
private $size;
|
||||
private $closed = false;
|
||||
|
||||
public function __construct(ReadableStreamInterface $input, $size = null)
|
||||
{
|
||||
$this->input = $input;
|
||||
$this->size = $size;
|
||||
|
||||
$that = $this;
|
||||
$pos =& $this->position;
|
||||
$input->on('data', function ($data) use ($that, &$pos, $size) {
|
||||
$that->emit('data', array($data));
|
||||
|
||||
$pos += \strlen($data);
|
||||
if ($size !== null && $pos >= $size) {
|
||||
$that->handleEnd();
|
||||
}
|
||||
});
|
||||
$input->on('error', function ($error) use ($that) {
|
||||
$that->emit('error', array($error));
|
||||
$that->close();
|
||||
});
|
||||
$input->on('end', array($that, 'handleEnd'));
|
||||
$input->on('close', array($that, 'close'));
|
||||
}
|
||||
|
||||
public function close()
|
||||
{
|
||||
if (!$this->closed) {
|
||||
$this->closed = true;
|
||||
$this->input->close();
|
||||
|
||||
$this->emit('close');
|
||||
$this->removeAllListeners();
|
||||
}
|
||||
}
|
||||
|
||||
public function isReadable()
|
||||
{
|
||||
return $this->input->isReadable();
|
||||
}
|
||||
|
||||
public function pause()
|
||||
{
|
||||
$this->input->pause();
|
||||
}
|
||||
|
||||
public function resume()
|
||||
{
|
||||
$this->input->resume();
|
||||
}
|
||||
|
||||
public function pipe(WritableStreamInterface $dest, array $options = array())
|
||||
{
|
||||
Util::pipe($this, $dest, $options);
|
||||
|
||||
return $dest;
|
||||
}
|
||||
|
||||
public function eof()
|
||||
{
|
||||
return !$this->isReadable();
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function detach()
|
||||
{
|
||||
throw new \BadMethodCallException();
|
||||
}
|
||||
|
||||
public function getSize()
|
||||
{
|
||||
return $this->size;
|
||||
}
|
||||
|
||||
public function tell()
|
||||
{
|
||||
throw new \BadMethodCallException();
|
||||
}
|
||||
|
||||
public function isSeekable()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function seek($offset, $whence = SEEK_SET)
|
||||
{
|
||||
throw new \BadMethodCallException();
|
||||
}
|
||||
|
||||
public function rewind()
|
||||
{
|
||||
throw new \BadMethodCallException();
|
||||
}
|
||||
|
||||
public function isWritable()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function write($string)
|
||||
{
|
||||
throw new \BadMethodCallException();
|
||||
}
|
||||
|
||||
public function read($length)
|
||||
{
|
||||
throw new \BadMethodCallException();
|
||||
}
|
||||
|
||||
public function getContents()
|
||||
{
|
||||
throw new \BadMethodCallException();
|
||||
}
|
||||
|
||||
public function getMetadata($key = null)
|
||||
{
|
||||
return ($key === null) ? array() : null;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
public function handleEnd()
|
||||
{
|
||||
if ($this->position !== $this->size && $this->size !== null) {
|
||||
$this->emit('error', array(new \UnderflowException('Unexpected end of response body after ' . $this->position . '/' . $this->size . ' bytes')));
|
||||
} else {
|
||||
$this->emit('end');
|
||||
}
|
||||
|
||||
$this->close();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
namespace Clue\React\Buzz\Message;
|
||||
|
||||
use RuntimeException;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
/**
|
||||
* The `ResponseException` is an `Exception` sub-class that will be used to reject
|
||||
* a request promise if the remote server returns a non-success status code
|
||||
* (anything but 2xx or 3xx).
|
||||
* You can control this behavior via the [`withRejectErrorResponse()` method](#withrejecterrorresponse).
|
||||
*
|
||||
* The `getCode(): int` method can be used to
|
||||
* return the HTTP response status code.
|
||||
*/
|
||||
class ResponseException extends RuntimeException
|
||||
{
|
||||
private $response;
|
||||
|
||||
public function __construct(ResponseInterface $response, $message = null, $code = null, $previous = null)
|
||||
{
|
||||
if ($message === null) {
|
||||
$message = 'HTTP status code ' . $response->getStatusCode() . ' (' . $response->getReasonPhrase() . ')';
|
||||
}
|
||||
if ($code === null) {
|
||||
$code = $response->getStatusCode();
|
||||
}
|
||||
parent::__construct($message, $code, $previous);
|
||||
|
||||
$this->response = $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Access its underlying [`ResponseInterface`](#responseinterface) object.
|
||||
*
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function getResponse()
|
||||
{
|
||||
return $this->response;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013 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.
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"name": "clue/connection-manager-extra",
|
||||
"description": "Extra decorators for creating async TCP/IP connections, built on top of ReactPHP's Socket component",
|
||||
"keywords": ["Socket", "network", "connection", "timeout", "delay", "reject", "repeat", "retry", "random", "acl", "firewall", "ReactPHP"],
|
||||
"homepage": "https://github.com/clue/reactphp-connection-manager-extra",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Christian Lück",
|
||||
"email": "christian@clue.engineering"
|
||||
}
|
||||
],
|
||||
"autoload": {
|
||||
"psr-4": { "ConnectionManager\\Extra\\": "src" }
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": { "ConnectionManager\\Tests\\Extra\\": "tests/" }
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3",
|
||||
"react/event-loop": "^1.2",
|
||||
"react/promise": "^3 || ^2.1 || ^1.2.1",
|
||||
"react/promise-timer": "^1.9",
|
||||
"react/socket": "^1.12"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^9.3 || ^5.7 || ^4.8"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
namespace ConnectionManager\Extra;
|
||||
|
||||
use React\EventLoop\Loop;
|
||||
use React\EventLoop\LoopInterface;
|
||||
use React\Promise\Timer;
|
||||
use React\Socket\ConnectorInterface;
|
||||
|
||||
class ConnectionManagerDelay implements ConnectorInterface
|
||||
{
|
||||
/** @var ConnectorInterface */
|
||||
private $connectionManager;
|
||||
|
||||
/** @var float */
|
||||
private $delay;
|
||||
|
||||
/** @var LoopInterface */
|
||||
private $loop;
|
||||
|
||||
/**
|
||||
* @param ConnectorInterface $connectionManager
|
||||
* @param float $delay
|
||||
* @param ?LoopInterface $loop
|
||||
*/
|
||||
public function __construct(ConnectorInterface $connectionManager, $delay, LoopInterface $loop = null)
|
||||
{
|
||||
$this->connectionManager = $connectionManager;
|
||||
$this->delay = $delay;
|
||||
$this->loop = $loop ?: Loop::get();
|
||||
}
|
||||
|
||||
public function connect($uri)
|
||||
{
|
||||
$connectionManager = $this->connectionManager;
|
||||
|
||||
return Timer\resolve($this->delay, $this->loop)->then(function () use ($connectionManager, $uri) {
|
||||
return $connectionManager->connect($uri);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
namespace ConnectionManager\Extra;
|
||||
|
||||
use React\Socket\ConnectorInterface;
|
||||
use React\Promise;
|
||||
use Exception;
|
||||
|
||||
// a simple connection manager that rejects every single connection attempt
|
||||
class ConnectionManagerReject implements ConnectorInterface
|
||||
{
|
||||
private $reason = 'Connection rejected';
|
||||
|
||||
/**
|
||||
* @param null|string|callable $reason
|
||||
*/
|
||||
public function __construct($reason = null)
|
||||
{
|
||||
if ($reason !== null) {
|
||||
$this->reason = $reason;
|
||||
}
|
||||
}
|
||||
|
||||
public function connect($uri)
|
||||
{
|
||||
$reason = $this->reason;
|
||||
if (!is_string($reason)) {
|
||||
try {
|
||||
$reason = $reason($uri);
|
||||
} catch (\Exception $e) {
|
||||
$reason = $e;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$reason instanceof \Exception) {
|
||||
$reason = new Exception($reason);
|
||||
}
|
||||
|
||||
return Promise\reject($reason);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
|
||||
namespace ConnectionManager\Extra;
|
||||
|
||||
use React\Socket\ConnectorInterface;
|
||||
use InvalidArgumentException;
|
||||
use Exception;
|
||||
use React\Promise\Promise;
|
||||
use React\Promise\PromiseInterface;
|
||||
|
||||
class ConnectionManagerRepeat implements ConnectorInterface
|
||||
{
|
||||
protected $connectionManager;
|
||||
protected $maximumTries;
|
||||
|
||||
public function __construct(ConnectorInterface $connectionManager, $maximumTries)
|
||||
{
|
||||
if ($maximumTries < 1) {
|
||||
throw new InvalidArgumentException('Maximum number of tries must be >= 1');
|
||||
}
|
||||
$this->connectionManager = $connectionManager;
|
||||
$this->maximumTries = $maximumTries;
|
||||
}
|
||||
|
||||
public function connect($uri)
|
||||
{
|
||||
$tries = $this->maximumTries;
|
||||
$connector = $this->connectionManager;
|
||||
|
||||
return new Promise(function ($resolve, $reject) use ($uri, &$pending, &$tries, $connector) {
|
||||
$try = function ($error = null) use (&$try, &$pending, &$tries, $uri, $connector, $resolve, $reject) {
|
||||
if ($tries > 0) {
|
||||
--$tries;
|
||||
$pending = $connector->connect($uri);
|
||||
$pending->then($resolve, $try);
|
||||
} else {
|
||||
$reject(new Exception('Connection still fails even after retrying', 0, $error));
|
||||
}
|
||||
};
|
||||
|
||||
$try();
|
||||
}, function ($_, $reject) use (&$pending, &$tries) {
|
||||
// stop retrying, reject results and cancel pending attempt
|
||||
$tries = 0;
|
||||
$reject(new \RuntimeException('Cancelled'));
|
||||
|
||||
if ($pending instanceof PromiseInterface && \method_exists($pending, 'cancel')) {
|
||||
$pending->cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
namespace ConnectionManager\Extra;
|
||||
|
||||
use React\Socket\ConnectorInterface;
|
||||
|
||||
// connection manager decorator which simplifies exchanging the actual connection manager during runtime
|
||||
class ConnectionManagerSwappable implements ConnectorInterface
|
||||
{
|
||||
protected $connectionManager;
|
||||
|
||||
public function __construct(ConnectorInterface $connectionManager)
|
||||
{
|
||||
$this->connectionManager = $connectionManager;
|
||||
}
|
||||
|
||||
public function connect($uri)
|
||||
{
|
||||
return $this->connectionManager->connect($uri);
|
||||
}
|
||||
|
||||
public function setConnectionManager(ConnectorInterface $connectionManager)
|
||||
{
|
||||
$this->connectionManager = $connectionManager;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
namespace ConnectionManager\Extra;
|
||||
|
||||
use React\EventLoop\Loop;
|
||||
use React\EventLoop\LoopInterface;
|
||||
use React\Promise\Timer;
|
||||
use React\Socket\ConnectorInterface;
|
||||
|
||||
class ConnectionManagerTimeout implements ConnectorInterface
|
||||
{
|
||||
/** @var ConnectorInterface */
|
||||
private $connectionManager;
|
||||
|
||||
/** @var float */
|
||||
private $timeout;
|
||||
|
||||
/** @var LoopInterface */
|
||||
private $loop;
|
||||
|
||||
/**
|
||||
* @param ConnectorInterface $connectionManager
|
||||
* @param float $timeout
|
||||
* @param ?LoopInterface $loop
|
||||
*/
|
||||
public function __construct(ConnectorInterface $connectionManager, $timeout, LoopInterface $loop = null)
|
||||
{
|
||||
$this->connectionManager = $connectionManager;
|
||||
$this->timeout = $timeout;
|
||||
$this->loop = $loop ?: Loop::get();
|
||||
}
|
||||
|
||||
public function connect($uri)
|
||||
{
|
||||
$promise = $this->connectionManager->connect($uri);
|
||||
|
||||
return Timer\timeout($promise, $this->timeout, $this->loop)->then(null, function ($e) use ($promise) {
|
||||
// connection successfully established but timeout already expired => close successful connection
|
||||
$promise->then(function ($connection) {
|
||||
$connection->end();
|
||||
});
|
||||
|
||||
throw $e;
|
||||
});
|
||||
}
|
||||
}
|
34
vendor/clue/connection-manager-extra/src/Multiple/ConnectionManagerConcurrent.php
vendored
Normal file
34
vendor/clue/connection-manager-extra/src/Multiple/ConnectionManagerConcurrent.php
vendored
Normal file
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
namespace ConnectionManager\Extra\Multiple;
|
||||
|
||||
use React\Promise;
|
||||
use React\Promise\PromiseInterface;
|
||||
|
||||
class ConnectionManagerConcurrent extends ConnectionManagerConsecutive
|
||||
{
|
||||
public function connect($uri)
|
||||
{
|
||||
$all = array();
|
||||
foreach ($this->managers as $connector) {
|
||||
$all []= $connector->connect($uri);
|
||||
}
|
||||
return Promise\any($all)->then(function ($conn) use ($all) {
|
||||
// a connection attempt succeeded
|
||||
// => cancel all pending connection attempts
|
||||
foreach ($all as $promise) {
|
||||
if ($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')) {
|
||||
$promise->cancel();
|
||||
}
|
||||
|
||||
// if promise resolves despite cancellation, immediately close stream
|
||||
$promise->then(function ($stream) use ($conn) {
|
||||
if ($stream !== $conn) {
|
||||
$stream->close();
|
||||
}
|
||||
});
|
||||
}
|
||||
return $conn;
|
||||
});
|
||||
}
|
||||
}
|
62
vendor/clue/connection-manager-extra/src/Multiple/ConnectionManagerConsecutive.php
vendored
Normal file
62
vendor/clue/connection-manager-extra/src/Multiple/ConnectionManagerConsecutive.php
vendored
Normal file
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
|
||||
namespace ConnectionManager\Extra\Multiple;
|
||||
|
||||
use React\Promise;
|
||||
use React\Promise\PromiseInterface;
|
||||
use React\Socket\ConnectorInterface;
|
||||
use UnderflowException;
|
||||
|
||||
class ConnectionManagerConsecutive implements ConnectorInterface
|
||||
{
|
||||
protected $managers;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param ConnectorInterface[] $managers
|
||||
*/
|
||||
public function __construct(array $managers)
|
||||
{
|
||||
if (!$managers) {
|
||||
throw new \InvalidArgumentException('List of connectors must not be empty');
|
||||
}
|
||||
$this->managers = $managers;
|
||||
}
|
||||
|
||||
public function connect($uri)
|
||||
{
|
||||
return $this->tryConnection($this->managers, $uri);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param ConnectorInterface[] $managers
|
||||
* @param string $uri
|
||||
* @return Promise
|
||||
* @internal
|
||||
*/
|
||||
public function tryConnection(array $managers, $uri)
|
||||
{
|
||||
return new Promise\Promise(function ($resolve, $reject) use (&$managers, &$pending, $uri) {
|
||||
$try = function () use (&$try, &$managers, $uri, $resolve, $reject, &$pending) {
|
||||
if (!$managers) {
|
||||
return $reject(new UnderflowException('No more managers to try to connect through'));
|
||||
}
|
||||
|
||||
$manager = array_shift($managers);
|
||||
$pending = $manager->connect($uri);
|
||||
$pending->then($resolve, $try);
|
||||
};
|
||||
|
||||
$try();
|
||||
}, function ($_, $reject) use (&$managers, &$pending) {
|
||||
// stop retrying, reject results and cancel pending attempt
|
||||
$managers = array();
|
||||
$reject(new \RuntimeException('Cancelled'));
|
||||
|
||||
if ($pending instanceof PromiseInterface && \method_exists($pending, 'cancel')) {
|
||||
$pending->cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
14
vendor/clue/connection-manager-extra/src/Multiple/ConnectionManagerRandom.php
vendored
Normal file
14
vendor/clue/connection-manager-extra/src/Multiple/ConnectionManagerRandom.php
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
|
||||
namespace ConnectionManager\Extra\Multiple;
|
||||
|
||||
class ConnectionManagerRandom extends ConnectionManagerConsecutive
|
||||
{
|
||||
public function connect($uri)
|
||||
{
|
||||
$managers = $this->managers;
|
||||
shuffle($managers);
|
||||
|
||||
return $this->tryConnection($managers, $uri);
|
||||
}
|
||||
}
|
111
vendor/clue/connection-manager-extra/src/Multiple/ConnectionManagerSelective.php
vendored
Normal file
111
vendor/clue/connection-manager-extra/src/Multiple/ConnectionManagerSelective.php
vendored
Normal file
|
@ -0,0 +1,111 @@
|
|||
<?php
|
||||
|
||||
namespace ConnectionManager\Extra\Multiple;
|
||||
|
||||
use React\Socket\ConnectorInterface;
|
||||
use React\Promise;
|
||||
use UnderflowException;
|
||||
use InvalidArgumentException;
|
||||
|
||||
class ConnectionManagerSelective implements ConnectorInterface
|
||||
{
|
||||
private $managers;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param ConnectorInterface[] $managers
|
||||
*/
|
||||
public function __construct(array $managers)
|
||||
{
|
||||
foreach ($managers as $filter => $manager) {
|
||||
$host = $filter;
|
||||
$portMin = 0;
|
||||
$portMax = 65535;
|
||||
|
||||
// search colon (either single one OR preceded by "]" due to IPv6)
|
||||
$colon = strrpos($host, ':');
|
||||
if ($colon !== false && (strpos($host, ':') === $colon || substr($host, $colon - 1, 1) === ']' )) {
|
||||
if (!isset($host[$colon + 1])) {
|
||||
throw new InvalidArgumentException('Entry "' . $filter . '" has no port after colon');
|
||||
}
|
||||
|
||||
$minus = strpos($host, '-', $colon);
|
||||
if ($minus === false) {
|
||||
$portMin = $portMax = (int)substr($host, $colon + 1);
|
||||
|
||||
if (substr($host, $colon + 1) !== (string)$portMin) {
|
||||
throw new InvalidArgumentException('Entry "' . $filter . '" has no valid port after colon');
|
||||
}
|
||||
} else {
|
||||
$portMin = (int)substr($host, $colon + 1, ($minus - $colon));
|
||||
$portMax = (int)substr($host, $minus + 1);
|
||||
|
||||
if (substr($host, $colon + 1) !== ($portMin . '-' . $portMax)) {
|
||||
throw new InvalidArgumentException('Entry "' . $filter . '" has no valid port range after colon');
|
||||
}
|
||||
|
||||
if ($portMin > $portMax) {
|
||||
throw new InvalidArgumentException('Entry "' . $filter . '" has port range mixed up');
|
||||
}
|
||||
}
|
||||
$host = substr($host, 0, $colon);
|
||||
}
|
||||
|
||||
if ($host === '') {
|
||||
throw new InvalidArgumentException('Entry "' . $filter . '" has an empty host');
|
||||
}
|
||||
|
||||
if (!$manager instanceof ConnectorInterface) {
|
||||
throw new InvalidArgumentException('Entry "' . $filter . '" is not a valid connector');
|
||||
}
|
||||
}
|
||||
|
||||
$this->managers = $managers;
|
||||
}
|
||||
|
||||
public function connect($uri)
|
||||
{
|
||||
$parts = parse_url((strpos($uri, '://') === false ? 'tcp://' : '') . $uri);
|
||||
if (!isset($parts) || !isset($parts['scheme'], $parts['host'], $parts['port'])) {
|
||||
return Promise\reject(new InvalidArgumentException('Invalid URI'));
|
||||
}
|
||||
|
||||
$connector = $this->getConnectorForTarget(
|
||||
trim($parts['host'], '[]'),
|
||||
$parts['port']
|
||||
);
|
||||
|
||||
if ($connector === null) {
|
||||
return Promise\reject(new UnderflowException('No connector for given target found'));
|
||||
}
|
||||
|
||||
return $connector->connect($uri);
|
||||
}
|
||||
|
||||
private function getConnectorForTarget($targetHost, $targetPort)
|
||||
{
|
||||
foreach ($this->managers as $host => $connector) {
|
||||
$portMin = 0;
|
||||
$portMax = 65535;
|
||||
|
||||
// search colon (either single one OR preceded by "]" due to IPv6)
|
||||
$colon = strrpos($host, ':');
|
||||
if ($colon !== false && (strpos($host, ':') === $colon || substr($host, $colon - 1, 1) === ']' )) {
|
||||
$minus = strpos($host, '-', $colon);
|
||||
if ($minus === false) {
|
||||
$portMin = $portMax = (int)substr($host, $colon + 1);
|
||||
} else {
|
||||
$portMin = (int)substr($host, $colon + 1, ($minus - $colon));
|
||||
$portMax = (int)substr($host, $minus + 1);
|
||||
}
|
||||
$host = trim(substr($host, 0, $colon), '[]');
|
||||
}
|
||||
|
||||
if ($targetPort >= $portMin && $targetPort <= $portMax && fnmatch($host, $targetHost)) {
|
||||
return $connector;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 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.
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"name": "clue/http-proxy-react",
|
||||
"description": "Async HTTP proxy connector, tunnel any TCP/IP-based protocol through an HTTP CONNECT proxy server, built on top of ReactPHP",
|
||||
"keywords": ["HTTP", "CONNECT", "proxy", "ReactPHP", "async"],
|
||||
"homepage": "https://github.com/clue/reactphp-http-proxy",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Christian Lück",
|
||||
"email": "christian@clue.engineering"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=5.3",
|
||||
"react/promise": "^3 || ^2.1 || ^1.2.1",
|
||||
"react/socket": "^1.12",
|
||||
"ringcentral/psr7": "^1.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"clue/block-react": "^1.5",
|
||||
"phpunit/phpunit": "^9.3 || ^5.7 || ^4.8",
|
||||
"react/event-loop": "^1.2",
|
||||
"react/http": "^1.5"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": { "Clue\\React\\HttpProxy\\": "src/" }
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": { "Clue\\Tests\\React\\HttpProxy\\": "tests/" }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,278 @@
|
|||
<?php
|
||||
|
||||
namespace Clue\React\HttpProxy;
|
||||
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
use RingCentral\Psr7;
|
||||
use React\Promise;
|
||||
use React\Promise\Deferred;
|
||||
use React\Socket\ConnectionInterface;
|
||||
use React\Socket\Connector;
|
||||
use React\Socket\ConnectorInterface;
|
||||
use React\Socket\FixedUriConnector;
|
||||
use React\Socket\UnixConnector;
|
||||
|
||||
/**
|
||||
* A simple Connector that uses an HTTP CONNECT proxy to create plain TCP/IP connections to any destination
|
||||
*
|
||||
* [you] -> [proxy] -> [destination]
|
||||
*
|
||||
* This is most frequently used to issue HTTPS requests to your destination.
|
||||
* However, this is actually performed on a higher protocol layer and this
|
||||
* connector is actually inherently a general-purpose plain TCP/IP connector.
|
||||
*
|
||||
* Note that HTTP CONNECT proxies often restrict which ports one may connect to.
|
||||
* Many (public) proxy servers do in fact limit this to HTTPS (443) only.
|
||||
*
|
||||
* If you want to establish a TLS connection (such as HTTPS) between you and
|
||||
* your destination, you may want to wrap this connector in a SecureConnector
|
||||
* instance.
|
||||
*
|
||||
* Note that communication between the client and the proxy is usually via an
|
||||
* unencrypted, plain TCP/IP HTTP connection. Note that this is the most common
|
||||
* setup, because you can still establish a TLS connection between you and the
|
||||
* destination host as above.
|
||||
*
|
||||
* If you want to connect to a (rather rare) HTTPS proxy, you may want use its
|
||||
* HTTPS port (443) and use a SecureConnector instance to create a secure
|
||||
* connection to the proxy.
|
||||
*
|
||||
* @link https://tools.ietf.org/html/rfc7231#section-4.3.6
|
||||
*/
|
||||
class ProxyConnector implements ConnectorInterface
|
||||
{
|
||||
private $connector;
|
||||
private $proxyUri;
|
||||
private $headers = '';
|
||||
|
||||
/**
|
||||
* Instantiate a new ProxyConnector which uses the given $proxyUrl
|
||||
*
|
||||
* @param string $proxyUrl The proxy URL may or may not contain a scheme and
|
||||
* port definition. The default port will be `80` for HTTP (or `443` for
|
||||
* HTTPS), but many common HTTP proxy servers use custom ports.
|
||||
* @param ?ConnectorInterface $connector (Optional) Connector to use.
|
||||
* @param array $httpHeaders Custom HTTP headers to be sent to the proxy.
|
||||
* @throws InvalidArgumentException if the proxy URL is invalid
|
||||
*/
|
||||
public function __construct(
|
||||
#[\SensitiveParameter]
|
||||
$proxyUrl,
|
||||
ConnectorInterface $connector = null,
|
||||
array $httpHeaders = array()
|
||||
) {
|
||||
// support `http+unix://` scheme for Unix domain socket (UDS) paths
|
||||
if (preg_match('/^http\+unix:\/\/(.*?@)?(.+?)$/', $proxyUrl, $match)) {
|
||||
// rewrite URI to parse authentication from dummy host
|
||||
$proxyUrl = 'http://' . $match[1] . 'localhost';
|
||||
|
||||
// connector uses Unix transport scheme and explicit path given
|
||||
$connector = new FixedUriConnector(
|
||||
'unix://' . $match[2],
|
||||
$connector ?: new UnixConnector()
|
||||
);
|
||||
}
|
||||
|
||||
if (strpos($proxyUrl, '://') === false) {
|
||||
$proxyUrl = 'http://' . $proxyUrl;
|
||||
}
|
||||
|
||||
$parts = parse_url($proxyUrl);
|
||||
if (!$parts || !isset($parts['scheme'], $parts['host']) || ($parts['scheme'] !== 'http' && $parts['scheme'] !== 'https')) {
|
||||
throw new InvalidArgumentException('Invalid proxy URL "' . $proxyUrl . '"');
|
||||
}
|
||||
|
||||
// apply default port and TCP/TLS transport for given scheme
|
||||
if (!isset($parts['port'])) {
|
||||
$parts['port'] = $parts['scheme'] === 'https' ? 443 : 80;
|
||||
}
|
||||
$parts['scheme'] = $parts['scheme'] === 'https' ? 'tls' : 'tcp';
|
||||
|
||||
$this->connector = $connector ?: new Connector();
|
||||
$this->proxyUri = $parts['scheme'] . '://' . $parts['host'] . ':' . $parts['port'];
|
||||
|
||||
// prepare Proxy-Authorization header if URI contains username/password
|
||||
if (isset($parts['user']) || isset($parts['pass'])) {
|
||||
$this->headers = 'Proxy-Authorization: Basic ' . base64_encode(
|
||||
rawurldecode($parts['user'] . ':' . (isset($parts['pass']) ? $parts['pass'] : ''))
|
||||
) . "\r\n";
|
||||
}
|
||||
|
||||
// append any additional custom request headers
|
||||
foreach ($httpHeaders as $name => $values) {
|
||||
foreach ((array)$values as $value) {
|
||||
$this->headers .= $name . ': ' . $value . "\r\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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('Invalid target URI specified'));
|
||||
}
|
||||
|
||||
$target = $parts['host'] . ':' . $parts['port'];
|
||||
|
||||
// construct URI to HTTP CONNECT proxy server to connect to
|
||||
$proxyUri = $this->proxyUri;
|
||||
|
||||
// append path from URI if given
|
||||
if (isset($parts['path'])) {
|
||||
$proxyUri .= $parts['path'];
|
||||
}
|
||||
|
||||
// parse query args
|
||||
$args = array();
|
||||
if (isset($parts['query'])) {
|
||||
parse_str($parts['query'], $args);
|
||||
}
|
||||
|
||||
// append hostname from URI to query string unless explicitly given
|
||||
if (!isset($args['hostname'])) {
|
||||
$args['hostname'] = trim($parts['host'], '[]');
|
||||
}
|
||||
|
||||
// append query string
|
||||
$proxyUri .= '?' . http_build_query($args, '', '&');
|
||||
|
||||
// append fragment from URI if given
|
||||
if (isset($parts['fragment'])) {
|
||||
$proxyUri .= '#' . $parts['fragment'];
|
||||
}
|
||||
|
||||
$connecting = $this->connector->connect($proxyUri);
|
||||
|
||||
$deferred = new Deferred(function ($_, $reject) use ($connecting, $uri) {
|
||||
$reject(new RuntimeException(
|
||||
'Connection to ' . $uri . ' cancelled while waiting for proxy (ECONNABORTED)',
|
||||
defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103
|
||||
));
|
||||
|
||||
// either close active connection or cancel pending connection attempt
|
||||
$connecting->then(function (ConnectionInterface $stream) {
|
||||
$stream->close();
|
||||
});
|
||||
$connecting->cancel();
|
||||
});
|
||||
|
||||
$headers = $this->headers;
|
||||
$connecting->then(function (ConnectionInterface $stream) use ($target, $headers, $deferred, $uri) {
|
||||
// keep buffering data until headers are complete
|
||||
$buffer = '';
|
||||
$stream->on('data', $fn = function ($chunk) use (&$buffer, $deferred, $stream, &$fn, $uri) {
|
||||
$buffer .= $chunk;
|
||||
|
||||
$pos = strpos($buffer, "\r\n\r\n");
|
||||
if ($pos !== false) {
|
||||
// end of headers received => stop buffering
|
||||
$stream->removeListener('data', $fn);
|
||||
$fn = null;
|
||||
|
||||
// try to parse headers as response message
|
||||
try {
|
||||
$response = Psr7\parse_response(substr($buffer, 0, $pos));
|
||||
} catch (Exception $e) {
|
||||
$deferred->reject(new RuntimeException(
|
||||
'Connection to ' . $uri . ' failed because proxy returned invalid response (EBADMSG)',
|
||||
defined('SOCKET_EBADMSG') ? SOCKET_EBADMSG: 71,
|
||||
$e
|
||||
));
|
||||
$stream->close();
|
||||
return;
|
||||
}
|
||||
|
||||
if ($response->getStatusCode() === 407) {
|
||||
// map status code 407 (Proxy Authentication Required) to EACCES
|
||||
$deferred->reject(new RuntimeException(
|
||||
'Connection to ' . $uri . ' failed because proxy denied access with HTTP error code ' . $response->getStatusCode() . ' (' . $response->getReasonPhrase() . ') (EACCES)',
|
||||
defined('SOCKET_EACCES') ? SOCKET_EACCES : 13
|
||||
));
|
||||
$stream->close();
|
||||
return;
|
||||
} elseif ($response->getStatusCode() < 200 || $response->getStatusCode() >= 300) {
|
||||
// map non-2xx status code to ECONNREFUSED
|
||||
$deferred->reject(new RuntimeException(
|
||||
'Connection to ' . $uri . ' failed because proxy refused connection with HTTP error code ' . $response->getStatusCode() . ' (' . $response->getReasonPhrase() . ') (ECONNREFUSED)',
|
||||
defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111
|
||||
));
|
||||
$stream->close();
|
||||
return;
|
||||
}
|
||||
|
||||
// all okay, resolve with stream instance
|
||||
$deferred->resolve($stream);
|
||||
|
||||
// emit remaining incoming as data event
|
||||
$buffer = (string)substr($buffer, $pos + 4);
|
||||
if ($buffer !== '') {
|
||||
$stream->emit('data', array($buffer));
|
||||
$buffer = '';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// stop buffering when 8 KiB have been read
|
||||
if (isset($buffer[8192])) {
|
||||
$deferred->reject(new RuntimeException(
|
||||
'Connection to ' . $uri . ' failed because proxy response headers exceed maximum of 8 KiB (EMSGSIZE)',
|
||||
defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 90
|
||||
));
|
||||
$stream->close();
|
||||
}
|
||||
});
|
||||
|
||||
$stream->on('error', function (Exception $e) use ($deferred, $uri) {
|
||||
$deferred->reject(new RuntimeException(
|
||||
'Connection to ' . $uri . ' failed because connection to proxy caused a stream error (EIO)',
|
||||
defined('SOCKET_EIO') ? SOCKET_EIO : 5,
|
||||
$e
|
||||
));
|
||||
});
|
||||
|
||||
$stream->on('close', function () use ($deferred, $uri) {
|
||||
$deferred->reject(new RuntimeException(
|
||||
'Connection to ' . $uri . ' failed because connection to proxy was lost while waiting for response (ECONNRESET)',
|
||||
defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 104
|
||||
));
|
||||
});
|
||||
|
||||
$stream->write("CONNECT " . $target . " HTTP/1.1\r\nHost: " . $target . "\r\n" . $headers . "\r\n");
|
||||
}, function (Exception $e) use ($deferred, $uri) {
|
||||
$deferred->reject($e = new RuntimeException(
|
||||
'Connection to ' . $uri . ' failed because connection to proxy failed (ECONNREFUSED)',
|
||||
defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111,
|
||||
$e
|
||||
));
|
||||
|
||||
// avoid garbage references by replacing all closures in call stack.
|
||||
// what a lovely piece of code!
|
||||
$r = new \ReflectionProperty('Exception', 'trace');
|
||||
$r->setAccessible(true);
|
||||
$trace = $r->getValue($e);
|
||||
|
||||
// Exception trace arguments are not available on some PHP 7.4 installs
|
||||
// @codeCoverageIgnoreStart
|
||||
foreach ($trace as $ti => $one) {
|
||||
if (isset($one['args'])) {
|
||||
foreach ($one['args'] as $ai => $arg) {
|
||||
if ($arg instanceof \Closure) {
|
||||
$trace[$ti]['args'][$ai] = 'Object(' . get_class($arg) . ')';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
$r->setValue($e, $trace);
|
||||
});
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018 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.
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"name": "clue/mq-react",
|
||||
"description": "Mini Queue, the lightweight in-memory message queue to concurrently do many (but not too many) things at once, built on top of ReactPHP",
|
||||
"keywords": ["Message Queue", "Mini Queue", "job", "message", "worker", "queue", "rate limit", "throttle", "concurrency", "ReactPHP", "async"],
|
||||
"homepage": "https://github.com/clue/reactphp-mq",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Christian Lück",
|
||||
"email": "christian@clue.engineering"
|
||||
}
|
||||
],
|
||||
"autoload": {
|
||||
"psr-4": { "Clue\\React\\Mq\\": "src/" }
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": { "Clue\\Tests\\React\\Mq\\": "tests/" }
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3",
|
||||
"react/promise": "^3 || ^2.2.1 || ^1.2.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35",
|
||||
"react/async": "^4 || ^3 || ^2",
|
||||
"react/event-loop": "^1.2",
|
||||
"react/http": "^1.8"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,447 @@
|
|||
<?php
|
||||
|
||||
namespace Clue\React\Mq;
|
||||
|
||||
use React\Promise;
|
||||
use React\Promise\Deferred;
|
||||
use React\Promise\PromiseInterface;
|
||||
|
||||
/**
|
||||
* The `Queue` is responsible for managing your operations and ensuring not too
|
||||
* many operations are executed at once. It's a very simple and lightweight
|
||||
* in-memory implementation of the
|
||||
* [leaky bucket](https://en.wikipedia.org/wiki/Leaky_bucket#As_a_queue) algorithm.
|
||||
*
|
||||
* This means that you control how many operations can be executed concurrently.
|
||||
* If you add a job to the queue and it still below the limit, it will be executed
|
||||
* immediately. If you keep adding new jobs to the queue and its concurrency limit
|
||||
* is reached, it will not start a new operation and instead queue this for future
|
||||
* execution. Once one of the pending operations complete, it will pick the next
|
||||
* job from the queue and execute this operation.
|
||||
*/
|
||||
class Queue implements \Countable
|
||||
{
|
||||
private $concurrency;
|
||||
private $limit;
|
||||
private $handler;
|
||||
|
||||
private $pending = 0;
|
||||
private $queue = array();
|
||||
|
||||
/**
|
||||
* Concurrently process all given jobs through the given `$handler`.
|
||||
*
|
||||
* This is a convenience method which uses the `Queue` internally to
|
||||
* schedule all jobs while limiting concurrency to ensure no more than
|
||||
* `$concurrency` jobs ever run at once. It will return a promise which
|
||||
* resolves with the results of all jobs on success.
|
||||
*
|
||||
* ```php
|
||||
* $browser = new React\Http\Browser();
|
||||
*
|
||||
* $promise = Queue::all(3, $urls, function ($url) use ($browser) {
|
||||
* return $browser->get($url);
|
||||
* });
|
||||
*
|
||||
* $promise->then(function (array $responses) {
|
||||
* echo 'All ' . count($responses) . ' successful!' . PHP_EOL;
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* If either of the jobs fail, it will reject the resulting promise and will
|
||||
* try to cancel all outstanding jobs. Similarly, calling `cancel()` on the
|
||||
* resulting promise will try to cancel all outstanding jobs. See
|
||||
* [promises](#promises) and [cancellation](#cancellation) for details.
|
||||
*
|
||||
* The `$concurrency` parameter sets a new soft limit for the maximum number
|
||||
* of jobs to handle concurrently. Finding a good concurrency limit depends
|
||||
* on your particular use case. It's common to limit concurrency to a rather
|
||||
* small value, as doing more than a dozen of things at once may easily
|
||||
* overwhelm the receiving side. Using a `1` value will ensure that all jobs
|
||||
* are processed one after another, effectively creating a "waterfall" of
|
||||
* jobs. Using a value less than 1 will reject with an
|
||||
* `InvalidArgumentException` without processing any jobs.
|
||||
*
|
||||
* ```php
|
||||
* // handle up to 10 jobs concurrently
|
||||
* $promise = Queue::all(10, $jobs, $handler);
|
||||
* ```
|
||||
*
|
||||
* ```php
|
||||
* // handle each job after another without concurrency (waterfall)
|
||||
* $promise = Queue::all(1, $jobs, $handler);
|
||||
* ```
|
||||
*
|
||||
* The `$jobs` parameter must be an array with all jobs to process. Each
|
||||
* value in this array will be passed to the `$handler` to start one job.
|
||||
* The array keys will be preserved in the resulting array, while the array
|
||||
* values will be replaced with the job results as returned by the
|
||||
* `$handler`. If this array is empty, this method will resolve with an
|
||||
* empty array without processing any jobs.
|
||||
*
|
||||
* The `$handler` parameter must be a valid callable that accepts your job
|
||||
* parameters, invokes the appropriate operation and returns a Promise as a
|
||||
* placeholder for its future result. If the given argument is not a valid
|
||||
* callable, this method will reject with an `InvalidArgumentException`
|
||||
* without processing any jobs.
|
||||
*
|
||||
* ```php
|
||||
* // using a Closure as handler is usually recommended
|
||||
* $promise = Queue::all(10, $jobs, function ($url) use ($browser) {
|
||||
* return $browser->get($url);
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* ```php
|
||||
* // accepts any callable, so PHP's array notation is also supported
|
||||
* $promise = Queue::all(10, $jobs, array($browser, 'get'));
|
||||
* ```
|
||||
*
|
||||
* > Keep in mind that returning an array of response messages means that
|
||||
* the whole response body has to be kept in memory.
|
||||
*
|
||||
* @param int $concurrency concurrency soft limit
|
||||
* @param array $jobs
|
||||
* @param callable $handler
|
||||
* @return PromiseInterface Returns a Promise<mixed[]> which resolves with an array of all resolution values
|
||||
* or rejects when any of the operations reject.
|
||||
*/
|
||||
public static function all($concurrency, array $jobs, $handler)
|
||||
{
|
||||
try {
|
||||
// limit number of concurrent operations
|
||||
$q = new self($concurrency, null, $handler);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
// reject if $concurrency or $handler is invalid
|
||||
return Promise\reject($e);
|
||||
}
|
||||
|
||||
// try invoking all operations and automatically queue excessive ones
|
||||
$promises = array_map($q, $jobs);
|
||||
|
||||
return new Promise\Promise(function ($resolve, $reject) use ($promises) {
|
||||
Promise\all($promises)->then($resolve, function ($e) use ($promises, $reject) {
|
||||
// cancel all pending promises if a single promise fails
|
||||
foreach (array_reverse($promises) as $promise) {
|
||||
if ($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')) {
|
||||
$promise->cancel();
|
||||
}
|
||||
}
|
||||
|
||||
// reject with original rejection message
|
||||
$reject($e);
|
||||
});
|
||||
}, function () use ($promises) {
|
||||
// cancel all pending promises on cancellation
|
||||
foreach (array_reverse($promises) as $promise) {
|
||||
if ($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')) {
|
||||
$promise->cancel();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Concurrently process the given jobs through the given `$handler` and
|
||||
* resolve with first resolution value.
|
||||
*
|
||||
* This is a convenience method which uses the `Queue` internally to
|
||||
* schedule all jobs while limiting concurrency to ensure no more than
|
||||
* `$concurrency` jobs ever run at once. It will return a promise which
|
||||
* resolves with the result of the first job on success and will then try
|
||||
* to `cancel()` all outstanding jobs.
|
||||
*
|
||||
* ```php
|
||||
* $browser = new React\Http\Browser();
|
||||
*
|
||||
* $promise = Queue::any(3, $urls, function ($url) use ($browser) {
|
||||
* return $browser->get($url);
|
||||
* });
|
||||
*
|
||||
* $promise->then(function (ResponseInterface $response) {
|
||||
* echo 'First response: ' . $response->getBody() . PHP_EOL;
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* If all of the jobs fail, it will reject the resulting promise. Similarly,
|
||||
* calling `cancel()` on the resulting promise will try to cancel all
|
||||
* outstanding jobs. See [promises](#promises) and
|
||||
* [cancellation](#cancellation) for details.
|
||||
*
|
||||
* The `$concurrency` parameter sets a new soft limit for the maximum number
|
||||
* of jobs to handle concurrently. Finding a good concurrency limit depends
|
||||
* on your particular use case. It's common to limit concurrency to a rather
|
||||
* small value, as doing more than a dozen of things at once may easily
|
||||
* overwhelm the receiving side. Using a `1` value will ensure that all jobs
|
||||
* are processed one after another, effectively creating a "waterfall" of
|
||||
* jobs. Using a value less than 1 will reject with an
|
||||
* `InvalidArgumentException` without processing any jobs.
|
||||
*
|
||||
* ```php
|
||||
* // handle up to 10 jobs concurrently
|
||||
* $promise = Queue::any(10, $jobs, $handler);
|
||||
* ```
|
||||
*
|
||||
* ```php
|
||||
* // handle each job after another without concurrency (waterfall)
|
||||
* $promise = Queue::any(1, $jobs, $handler);
|
||||
* ```
|
||||
*
|
||||
* The `$jobs` parameter must be an array with all jobs to process. Each
|
||||
* value in this array will be passed to the `$handler` to start one job.
|
||||
* The array keys have no effect, the promise will simply resolve with the
|
||||
* job results of the first successful job as returned by the `$handler`.
|
||||
* If this array is empty, this method will reject without processing any
|
||||
* jobs.
|
||||
*
|
||||
* The `$handler` parameter must be a valid callable that accepts your job
|
||||
* parameters, invokes the appropriate operation and returns a Promise as a
|
||||
* placeholder for its future result. If the given argument is not a valid
|
||||
* callable, this method will reject with an `InvalidArgumentExceptionn`
|
||||
* without processing any jobs.
|
||||
*
|
||||
* ```php
|
||||
* // using a Closure as handler is usually recommended
|
||||
* $promise = Queue::any(10, $jobs, function ($url) use ($browser) {
|
||||
* return $browser->get($url);
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* ```php
|
||||
* // accepts any callable, so PHP's array notation is also supported
|
||||
* $promise = Queue::any(10, $jobs, array($browser, 'get'));
|
||||
* ```
|
||||
*
|
||||
* @param int $concurrency concurrency soft limit
|
||||
* @param array $jobs
|
||||
* @param callable $handler
|
||||
* @return PromiseInterface Returns a Promise<mixed> which resolves with a single resolution value
|
||||
* or rejects when all of the operations reject.
|
||||
*/
|
||||
public static function any($concurrency, array $jobs, $handler)
|
||||
{
|
||||
// explicitly reject with empty jobs (https://github.com/reactphp/promise/pull/34)
|
||||
if (!$jobs) {
|
||||
return Promise\reject(new \UnderflowException('No jobs given'));
|
||||
}
|
||||
|
||||
try {
|
||||
// limit number of concurrent operations
|
||||
$q = new self($concurrency, null, $handler);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
// reject if $concurrency or $handler is invalid
|
||||
return Promise\reject($e);
|
||||
}
|
||||
|
||||
// try invoking all operations and automatically queue excessive ones
|
||||
$promises = array_map($q, $jobs);
|
||||
|
||||
return new Promise\Promise(function ($resolve, $reject) use ($promises) {
|
||||
Promise\any($promises)->then(function ($result) use ($promises, $resolve) {
|
||||
// cancel all pending promises if a single result is ready
|
||||
foreach (array_reverse($promises) as $promise) {
|
||||
if ($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')) {
|
||||
$promise->cancel();
|
||||
}
|
||||
}
|
||||
|
||||
// resolve with original resolution value
|
||||
$resolve($result);
|
||||
}, $reject);
|
||||
}, function () use ($promises) {
|
||||
// cancel all pending promises on cancellation
|
||||
foreach (array_reverse($promises) as $promise) {
|
||||
if ($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')) {
|
||||
$promise->cancel();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a new queue object.
|
||||
*
|
||||
* You can create any number of queues, for example when you want to apply
|
||||
* different limits to different kind of operations.
|
||||
*
|
||||
* The `$concurrency` parameter sets a new soft limit for the maximum number
|
||||
* of jobs to handle concurrently. Finding a good concurrency limit depends
|
||||
* on your particular use case. It's common to limit concurrency to a rather
|
||||
* small value, as doing more than a dozen of things at once may easily
|
||||
* overwhelm the receiving side.
|
||||
*
|
||||
* The `$limit` parameter sets a new hard limit on how many jobs may be
|
||||
* outstanding (kept in memory) at once. Depending on your particular use
|
||||
* case, it's usually safe to keep a few hundreds or thousands of jobs in
|
||||
* memory. If you do not want to apply an upper limit, you can pass a `null`
|
||||
* value which is semantically more meaningful than passing a big number.
|
||||
*
|
||||
* ```php
|
||||
* // handle up to 10 jobs concurrently, but keep no more than 1000 in memory
|
||||
* $q = new Queue(10, 1000, $handler);
|
||||
* ```
|
||||
*
|
||||
* ```php
|
||||
* // handle up to 10 jobs concurrently, do not limit queue size
|
||||
* $q = new Queue(10, null, $handler);
|
||||
* ```
|
||||
*
|
||||
* ```php
|
||||
* // handle up to 10 jobs concurrently, reject all further jobs
|
||||
* $q = new Queue(10, 10, $handler);
|
||||
* ```
|
||||
*
|
||||
* The `$handler` parameter must be a valid callable that accepts your job
|
||||
* parameters, invokes the appropriate operation and returns a Promise as a
|
||||
* placeholder for its future result.
|
||||
*
|
||||
* ```php
|
||||
* // using a Closure as handler is usually recommended
|
||||
* $q = new Queue(10, null, function ($url) use ($browser) {
|
||||
* return $browser->get($url);
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* ```php
|
||||
* // PHP's array callable as handler is also supported
|
||||
* $q = new Queue(10, null, array($browser, 'get'));
|
||||
* ```
|
||||
*
|
||||
* @param int $concurrency concurrency soft limit
|
||||
* @param int|null $limit queue hard limit or NULL=unlimited
|
||||
* @param callable $handler
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function __construct($concurrency, $limit, $handler)
|
||||
{
|
||||
if ($concurrency < 1 || ($limit !== null && ($limit < 1 || $concurrency > $limit))) {
|
||||
throw new \InvalidArgumentException('Invalid limit given');
|
||||
}
|
||||
if (!is_callable($handler)) {
|
||||
throw new \InvalidArgumentException('Invalid handler given');
|
||||
}
|
||||
|
||||
$this->concurrency = $concurrency;
|
||||
$this->limit = $limit;
|
||||
$this->handler = $handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Queue instance is invokable, so that invoking `$q(...$args)` will
|
||||
* actually be forwarded as `$handler(...$args)` as given in the
|
||||
* `$handler` argument when concurrency is still below limits.
|
||||
*
|
||||
* Each operation may take some time to complete, but due to its async nature you
|
||||
* can actually start any number of (queued) operations. Once the concurrency limit
|
||||
* is reached, this invocation will simply be queued and this will return a pending
|
||||
* promise which will start the actual operation once another operation is
|
||||
* completed. This means that this is handled entirely transparently and you do not
|
||||
* need to worry about this concurrency limit yourself.
|
||||
*
|
||||
* @return \React\Promise\PromiseInterface
|
||||
*/
|
||||
public function __invoke()
|
||||
{
|
||||
// happy path: simply invoke handler if we're below concurrency limit
|
||||
if ($this->pending < $this->concurrency) {
|
||||
++$this->pending;
|
||||
|
||||
// invoke handler and await its resolution before invoking next queued job
|
||||
return $this->await(
|
||||
call_user_func_array($this->handler, func_get_args())
|
||||
);
|
||||
}
|
||||
|
||||
// we're currently above concurrency limit, make sure we do not exceed maximum queue limit
|
||||
if ($this->limit !== null && $this->count() >= $this->limit) {
|
||||
return Promise\reject(new \OverflowException('Maximum queue limit of ' . $this->limit . ' exceeded'));
|
||||
}
|
||||
|
||||
// if we reach this point, then this job will need to be queued
|
||||
// get next queue position
|
||||
$queue =& $this->queue;
|
||||
$queue[] = null;
|
||||
end($queue);
|
||||
$id = key($queue);
|
||||
|
||||
$deferred = new Deferred(function ($_, $reject) use (&$queue, $id, &$deferred) {
|
||||
// forward cancellation to pending operation if it is currently executing
|
||||
if (isset($deferred->pending) && $deferred->pending instanceof PromiseInterface && \method_exists($deferred->pending, 'cancel')) {
|
||||
$deferred->pending->cancel();
|
||||
}
|
||||
unset($deferred->pending);
|
||||
|
||||
if (isset($deferred->args)) {
|
||||
// queued promise cancelled before its handler is invoked
|
||||
// remove from queue and reject explicitly
|
||||
unset($queue[$id], $deferred->args);
|
||||
$reject(new \RuntimeException('Cancelled queued job before processing started'));
|
||||
}
|
||||
});
|
||||
|
||||
// queue job to process if number of pending jobs is below concurrency limit again
|
||||
$deferred->args = func_get_args();
|
||||
$queue[$id] = $deferred;
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
#[\ReturnTypeWillChange]
|
||||
public function count()
|
||||
{
|
||||
return $this->pending + count($this->queue);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function await(PromiseInterface $promise)
|
||||
{
|
||||
$that = $this;
|
||||
|
||||
return $promise->then(function ($result) use ($that) {
|
||||
$that->processQueue();
|
||||
|
||||
return $result;
|
||||
}, function ($error) use ($that) {
|
||||
$that->processQueue();
|
||||
|
||||
return Promise\reject($error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function processQueue()
|
||||
{
|
||||
// skip if we're still above concurrency limit or there's no queued job waiting
|
||||
if (--$this->pending >= $this->concurrency || !$this->queue) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* @var $deferred Deferred */
|
||||
$deferred = reset($this->queue);
|
||||
unset($this->queue[key($this->queue)]);
|
||||
|
||||
// once number of pending jobs is below concurrency limit again:
|
||||
// await this situation, invoke handler and await its resolution before invoking next queued job
|
||||
++$this->pending;
|
||||
|
||||
$promise = call_user_func_array($this->handler, $deferred->args);
|
||||
$deferred->pending = $promise;
|
||||
unset($deferred->args);
|
||||
|
||||
// invoke handler and await its resolution before invoking next queued job
|
||||
$this->await($promise)->then(
|
||||
function ($result) use ($deferred) {
|
||||
unset($deferred->pending);
|
||||
$deferred->resolve($result);
|
||||
},
|
||||
function ($e) use ($deferred) {
|
||||
unset($deferred->pending);
|
||||
$deferred->reject($e);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "clue/redis-protocol",
|
||||
"description": "A streaming redis wire protocol parser and serializer implementation in PHP",
|
||||
"keywords": ["streaming", "redis", "protocol", "parser", "serializer"],
|
||||
"homepage": "https://github.com/clue/php-redis-protocol",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Christian Lück",
|
||||
"email": "christian@lueck.tv"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=5.3"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-0": { "Clue\\Redis\\Protocol": "src" }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
namespace Clue\Redis\Protocol;
|
||||
|
||||
use Clue\Redis\Protocol\Parser\ParserInterface;
|
||||
use Clue\Redis\Protocol\Parser\ResponseParser;
|
||||
use Clue\Redis\Protocol\Serializer\SerializerInterface;
|
||||
use Clue\Redis\Protocol\Serializer\RecursiveSerializer;
|
||||
use Clue\Redis\Protocol\Parser\RequestParser;
|
||||
|
||||
/**
|
||||
* Provides factory methods used to instantiate the best available protocol implementation
|
||||
*/
|
||||
class Factory
|
||||
{
|
||||
/**
|
||||
* instantiate the best available protocol response parser implementation
|
||||
*
|
||||
* This is the parser every redis client implementation should use in order
|
||||
* to parse incoming response messages from a redis server.
|
||||
*
|
||||
* @return ParserInterface
|
||||
*/
|
||||
public function createResponseParser()
|
||||
{
|
||||
return new ResponseParser();
|
||||
}
|
||||
|
||||
/**
|
||||
* instantiate the best available protocol request parser implementation
|
||||
*
|
||||
* This is most useful for a redis server implementation which needs to
|
||||
* process client requests.
|
||||
*
|
||||
* @return ParserInterface
|
||||
*/
|
||||
public function createRequestParser()
|
||||
{
|
||||
return new RequestParser();
|
||||
}
|
||||
|
||||
/**
|
||||
* instantiate the best available protocol serializer implementation
|
||||
*
|
||||
* @return SerializerInterface
|
||||
*/
|
||||
public function createSerializer()
|
||||
{
|
||||
return new RecursiveSerializer();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
namespace Clue\Redis\Protocol\Model;
|
||||
|
||||
use Clue\Redis\Protocol\Model\ModelInterface;
|
||||
use Clue\Redis\Protocol\Serializer\SerializerInterface;
|
||||
|
||||
class BulkReply implements ModelInterface
|
||||
{
|
||||
private $value;
|
||||
|
||||
/**
|
||||
* create bulk reply (string reply)
|
||||
*
|
||||
* @param string|null $data
|
||||
*/
|
||||
public function __construct($value)
|
||||
{
|
||||
if ($value !== null) {
|
||||
$value = (string)$value;
|
||||
}
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
public function getValueNative()
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function getMessageSerialized(SerializerInterface $serializer)
|
||||
{
|
||||
return $serializer->getBulkMessage($this->value);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
namespace Clue\Redis\Protocol\Model;
|
||||
|
||||
use Exception;
|
||||
use Clue\Redis\Protocol\Serializer\SerializerInterface;
|
||||
|
||||
/**
|
||||
*
|
||||
* @link http://redis.io/topics/protocol#status-reply
|
||||
*/
|
||||
class ErrorReply extends Exception implements ModelInterface
|
||||
{
|
||||
/**
|
||||
* create error status reply (single line error message)
|
||||
*
|
||||
* @param string|ErrorReplyException $message
|
||||
* @return string
|
||||
*/
|
||||
public function __construct($message, $code = 0, $previous = null)
|
||||
{
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
||||
public function getValueNative()
|
||||
{
|
||||
return $this->getMessage();
|
||||
}
|
||||
|
||||
public function getMessageSerialized(SerializerInterface $serializer)
|
||||
{
|
||||
return $serializer->getErrorMessage($this->getMessage());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
namespace Clue\Redis\Protocol\Model;
|
||||
|
||||
use Clue\Redis\Protocol\Model\ModelInterface;
|
||||
use Clue\Redis\Protocol\Serializer\SerializerInterface;
|
||||
|
||||
class IntegerReply implements ModelInterface
|
||||
{
|
||||
private $value;
|
||||
|
||||
/**
|
||||
* create integer reply
|
||||
*
|
||||
* @param int $data
|
||||
*/
|
||||
public function __construct($value)
|
||||
{
|
||||
$this->value = (int)$value;
|
||||
}
|
||||
|
||||
public function getValueNative()
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function getMessageSerialized(SerializerInterface $serializer)
|
||||
{
|
||||
return $serializer->getIntegerMessage($this->value);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
namespace Clue\Redis\Protocol\Model;
|
||||
|
||||
use Clue\Redis\Protocol\Serializer\SerializerInterface;
|
||||
|
||||
interface ModelInterface
|
||||
{
|
||||
/**
|
||||
* Returns value of this model as a native representation for PHP
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getValueNative();
|
||||
|
||||
/**
|
||||
* Returns the serialized representation of this protocol message
|
||||
*
|
||||
* @param SerializerInterface $serializer;
|
||||
* @return string
|
||||
*/
|
||||
public function getMessageSerialized(SerializerInterface $serializer);
|
||||
}
|
100
vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Model/MultiBulkReply.php
vendored
Normal file
100
vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Model/MultiBulkReply.php
vendored
Normal file
|
@ -0,0 +1,100 @@
|
|||
<?php
|
||||
|
||||
namespace Clue\Redis\Protocol\Model;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use UnexpectedValueException;
|
||||
use Clue\Redis\Protocol\Serializer\SerializerInterface;
|
||||
|
||||
class MultiBulkReply implements ModelInterface
|
||||
{
|
||||
/**
|
||||
* @var array|null
|
||||
*/
|
||||
private $data;
|
||||
|
||||
/**
|
||||
* create multi bulk reply (an array of other replies, usually bulk replies)
|
||||
*
|
||||
* @param array|null $data
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function __construct(array $data = null)
|
||||
{
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
public function getValueNative()
|
||||
{
|
||||
if ($this->data === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$ret = array();
|
||||
foreach ($this->data as $one) {
|
||||
if ($one instanceof ModelInterface) {
|
||||
$ret []= $one->getValueNative();
|
||||
} else {
|
||||
$ret []= $one;
|
||||
}
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
|
||||
public function getMessageSerialized(SerializerInterface $serializer)
|
||||
{
|
||||
return $serializer->getMultiBulkMessage($this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether this model represents a valid unified request protocol message
|
||||
*
|
||||
* The new unified protocol was introduced in Redis 1.2, but it became the
|
||||
* standard way for talking with the Redis server in Redis 2.0. The unified
|
||||
* request protocol is what Redis already uses in replies in order to send
|
||||
* list of items to clients, and is called a Multi Bulk Reply.
|
||||
*
|
||||
* @return boolean
|
||||
* @link http://redis.io/topics/protocol
|
||||
*/
|
||||
public function isRequest()
|
||||
{
|
||||
if (!$this->data) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($this->data as $one) {
|
||||
if (!($one instanceof BulkReply) && !is_string($one)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getRequestModel()
|
||||
{
|
||||
if (!$this->data) {
|
||||
throw new UnexpectedValueException('Null-multi-bulk message can not be represented as a request, must contain string/bulk values');
|
||||
}
|
||||
|
||||
$command = null;
|
||||
$args = array();
|
||||
|
||||
foreach ($this->data as $one) {
|
||||
if ($one instanceof BulkReply) {
|
||||
$one = $one->getValueNative();
|
||||
} elseif (!is_string($one)) {
|
||||
throw new UnexpectedValueException('Message can not be represented as a request, must only contain string/bulk values');
|
||||
}
|
||||
|
||||
if ($command === null) {
|
||||
$command = $one;
|
||||
} else {
|
||||
$args []= $one;
|
||||
}
|
||||
}
|
||||
|
||||
return new Request($command, $args);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
|
||||
namespace Clue\Redis\Protocol\Model;
|
||||
|
||||
use Clue\Redis\Protocol\Model\ModelInterface;
|
||||
use Clue\Redis\Protocol\Model\BulkReply;
|
||||
use Clue\Redis\Protocol\Model\MultiBulkReply;
|
||||
use Clue\Redis\Protocol\Serializer\SerializerInterface;
|
||||
|
||||
class Request implements ModelInterface
|
||||
{
|
||||
private $command;
|
||||
private $args;
|
||||
|
||||
public function __construct($command, array $args = array())
|
||||
{
|
||||
$this->command = $command;
|
||||
$this->args = $args;
|
||||
}
|
||||
|
||||
public function getCommand()
|
||||
{
|
||||
return $this->command;
|
||||
}
|
||||
|
||||
public function getArgs()
|
||||
{
|
||||
return $this->args;
|
||||
}
|
||||
|
||||
public function getReplyModel()
|
||||
{
|
||||
$models = array(new BulkReply($this->command));
|
||||
foreach ($this->args as $arg) {
|
||||
$models []= new BulkReply($arg);
|
||||
}
|
||||
|
||||
return new MultiBulkReply($models);
|
||||
}
|
||||
|
||||
public function getValueNative()
|
||||
{
|
||||
$ret = $this->args;
|
||||
array_unshift($ret, $this->command);
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
public function getMessageSerialized(SerializerInterface $serializer)
|
||||
{
|
||||
return $serializer->getRequestMessage($this->command, $this->args);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
namespace Clue\Redis\Protocol\Model;
|
||||
|
||||
use Clue\Redis\Protocol\Serializer\SerializerInterface;
|
||||
/**
|
||||
*
|
||||
* @link http://redis.io/topics/protocol#status-reply
|
||||
*/
|
||||
class StatusReply implements ModelInterface
|
||||
{
|
||||
private $message;
|
||||
|
||||
/**
|
||||
* create status reply (single line message)
|
||||
*
|
||||
* @param string|Status $message
|
||||
* @return string
|
||||
*/
|
||||
public function __construct($message)
|
||||
{
|
||||
$this->message = $message;
|
||||
}
|
||||
|
||||
public function getValueNative()
|
||||
{
|
||||
return $this->message;
|
||||
}
|
||||
|
||||
public function getMessageSerialized(SerializerInterface $serializer)
|
||||
{
|
||||
return $serializer->getStatusMessage($this->message);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
namespace Clue\Redis\Protocol\Parser;
|
||||
|
||||
use UnderflowException;
|
||||
|
||||
class MessageBuffer implements ParserInterface
|
||||
{
|
||||
private $parser;
|
||||
private $incomingQueue = array();
|
||||
|
||||
public function __construct(ParserInterface $parser)
|
||||
{
|
||||
$this->parser = $parser;
|
||||
}
|
||||
|
||||
public function popIncomingModel()
|
||||
{
|
||||
if (!$this->incomingQueue) {
|
||||
throw new UnderflowException('Incoming message queue is empty');
|
||||
}
|
||||
return array_shift($this->incomingQueue);
|
||||
}
|
||||
|
||||
public function hasIncomingModel()
|
||||
{
|
||||
return ($this->incomingQueue) ? true : false;
|
||||
}
|
||||
|
||||
public function pushIncoming($data)
|
||||
{
|
||||
$ret = $this->parser->pushIncoming($data);
|
||||
|
||||
foreach ($ret as $one) {
|
||||
$this->incomingQueue []= $one;
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
}
|
10
vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Parser/ParserException.php
vendored
Normal file
10
vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Parser/ParserException.php
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace Clue\Redis\Protocol\Parser;
|
||||
|
||||
use UnexpectedValueException;
|
||||
|
||||
class ParserException extends UnexpectedValueException
|
||||
{
|
||||
|
||||
}
|
28
vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Parser/ParserInterface.php
vendored
Normal file
28
vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Parser/ParserInterface.php
vendored
Normal file
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace Clue\Redis\Protocol\Parser;
|
||||
|
||||
use Clue\Redis\Protocol\Model\ModelInterface;
|
||||
use Clue\Redis\Protocol\Parser\ParserException;
|
||||
|
||||
interface ParserInterface
|
||||
{
|
||||
/**
|
||||
* push a chunk of the redis protocol message into the buffer and parse
|
||||
*
|
||||
* You can push any number of bytes of a redis protocol message into the
|
||||
* parser and it will try to parse messages from its data stream. So you can
|
||||
* pass data directly from your socket stream and the parser will return the
|
||||
* right amount of message model objects for you.
|
||||
*
|
||||
* If you pass an incomplete message, expect it to return an empty array. If
|
||||
* your incomplete message is split to across multiple chunks, the parsed
|
||||
* message model will be returned once the parser has sufficient data.
|
||||
*
|
||||
* @param string $dataChunk
|
||||
* @return ModelInterface[] 0+ message models
|
||||
* @throws ParserException if the message can not be parsed
|
||||
* @see self::popIncomingModel()
|
||||
*/
|
||||
public function pushIncoming($dataChunk);
|
||||
}
|
125
vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Parser/RequestParser.php
vendored
Normal file
125
vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Parser/RequestParser.php
vendored
Normal file
|
@ -0,0 +1,125 @@
|
|||
<?php
|
||||
|
||||
namespace Clue\Redis\Protocol\Parser;
|
||||
|
||||
use Clue\Redis\Protocol\Parser\ParserException;
|
||||
use Clue\Redis\Protocol\Model\Request;
|
||||
|
||||
class RequestParser implements ParserInterface
|
||||
{
|
||||
const CRLF = "\r\n";
|
||||
|
||||
private $incomingBuffer = '';
|
||||
private $incomingOffset = 0;
|
||||
|
||||
public function pushIncoming($dataChunk)
|
||||
{
|
||||
$this->incomingBuffer .= $dataChunk;
|
||||
|
||||
$parsed = array();
|
||||
|
||||
do {
|
||||
$saved = $this->incomingOffset;
|
||||
$message = $this->readRequest();
|
||||
if ($message === null) {
|
||||
// restore previous position for next parsing attempt
|
||||
$this->incomingOffset = $saved;
|
||||
break;
|
||||
}
|
||||
|
||||
if ($message !== false) {
|
||||
$parsed []= $message;
|
||||
}
|
||||
} while($this->incomingBuffer !== '');
|
||||
|
||||
if ($this->incomingOffset !== 0) {
|
||||
$this->incomingBuffer = (string)substr($this->incomingBuffer, $this->incomingOffset);
|
||||
$this->incomingOffset = 0;
|
||||
}
|
||||
|
||||
return $parsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* try to parse request from incoming buffer
|
||||
*
|
||||
* @throws ParserException if the incoming buffer is invalid
|
||||
* @return Request|null
|
||||
*/
|
||||
private function readRequest()
|
||||
{
|
||||
$crlf = strpos($this->incomingBuffer, "\r\n", $this->incomingOffset);
|
||||
if ($crlf === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// line starts with a multi-bulk header "*"
|
||||
if (isset($this->incomingBuffer[$this->incomingOffset]) && $this->incomingBuffer[$this->incomingOffset] === '*') {
|
||||
$line = substr($this->incomingBuffer, $this->incomingOffset + 1, $crlf - $this->incomingOffset + 1);
|
||||
$this->incomingOffset = $crlf + 2;
|
||||
$count = (int)$line;
|
||||
|
||||
if ($count <= 0) {
|
||||
return false;
|
||||
}
|
||||
$command = null;
|
||||
$args = array();
|
||||
for ($i = 0; $i < $count; ++$i) {
|
||||
$sub = $this->readBulk();
|
||||
if ($sub === null) {
|
||||
return null;
|
||||
}
|
||||
if ($command === null) {
|
||||
$command = $sub;
|
||||
} else {
|
||||
$args []= $sub;
|
||||
}
|
||||
}
|
||||
return new Request($command, $args);
|
||||
}
|
||||
|
||||
// parse an old inline request instead
|
||||
$line = substr($this->incomingBuffer, $this->incomingOffset, $crlf - $this->incomingOffset);
|
||||
$this->incomingOffset = $crlf + 2;
|
||||
|
||||
$args = preg_split('/ +/', trim($line, ' '));
|
||||
$command = array_shift($args);
|
||||
|
||||
if ($command === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return new Request($command, $args);
|
||||
}
|
||||
|
||||
private function readBulk()
|
||||
{
|
||||
$crlf = strpos($this->incomingBuffer, "\r\n", $this->incomingOffset);
|
||||
if ($crlf === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// line has to start with a bulk header "$"
|
||||
if (!isset($this->incomingBuffer[$this->incomingOffset]) || $this->incomingBuffer[$this->incomingOffset] !== '$') {
|
||||
throw new ParserException('ERR Protocol error: expected \'$\', got \'' . substr($this->incomingBuffer, $this->incomingOffset, 1) . '\'');
|
||||
}
|
||||
|
||||
$line = substr($this->incomingBuffer, $this->incomingOffset + 1, $crlf - $this->incomingOffset + 1);
|
||||
$this->incomingOffset = $crlf + 2;
|
||||
$size = (int)$line;
|
||||
|
||||
if ($size < 0) {
|
||||
throw new ParserException('ERR Protocol error: invalid bulk length');
|
||||
}
|
||||
|
||||
if (!isset($this->incomingBuffer[$this->incomingOffset + $size + 1])) {
|
||||
// check enough bytes + crlf are buffered
|
||||
return null;
|
||||
}
|
||||
|
||||
$ret = substr($this->incomingBuffer, $this->incomingOffset, $size);
|
||||
$this->incomingOffset += $size + 2;
|
||||
|
||||
return $ret;
|
||||
}
|
||||
}
|
151
vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Parser/ResponseParser.php
vendored
Normal file
151
vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Parser/ResponseParser.php
vendored
Normal file
|
@ -0,0 +1,151 @@
|
|||
<?php
|
||||
|
||||
namespace Clue\Redis\Protocol\Parser;
|
||||
|
||||
use Clue\Redis\Protocol\Parser\ParserInterface;
|
||||
use Clue\Redis\Protocol\Model\ModelInterface;
|
||||
use Clue\Redis\Protocol\Model\BulkReply;
|
||||
use Clue\Redis\Protocol\Model\ErrorReply;
|
||||
use Clue\Redis\Protocol\Model\IntegerReply;
|
||||
use Clue\Redis\Protocol\Model\MultiBulkReply;
|
||||
use Clue\Redis\Protocol\Model\StatusReply;
|
||||
use Clue\Redis\Protocol\Parser\ParserException;
|
||||
|
||||
/**
|
||||
* Simple recursive redis wire protocol parser
|
||||
*
|
||||
* Heavily influenced by blocking parser implementation from jpd/redisent.
|
||||
*
|
||||
* @link https://github.com/jdp/redisent
|
||||
* @link http://redis.io/topics/protocol
|
||||
*/
|
||||
class ResponseParser implements ParserInterface
|
||||
{
|
||||
const CRLF = "\r\n";
|
||||
|
||||
private $incomingBuffer = '';
|
||||
private $incomingOffset = 0;
|
||||
|
||||
public function pushIncoming($dataChunk)
|
||||
{
|
||||
$this->incomingBuffer .= $dataChunk;
|
||||
|
||||
return $this->tryParsingIncomingMessages();
|
||||
}
|
||||
|
||||
private function tryParsingIncomingMessages()
|
||||
{
|
||||
$messages = array();
|
||||
|
||||
do {
|
||||
$message = $this->readResponse();
|
||||
if ($message === null) {
|
||||
// restore previous position for next parsing attempt
|
||||
$this->incomingOffset = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
$messages []= $message;
|
||||
|
||||
$this->incomingBuffer = (string)substr($this->incomingBuffer, $this->incomingOffset);
|
||||
$this->incomingOffset = 0;
|
||||
} while($this->incomingBuffer !== '');
|
||||
|
||||
return $messages;
|
||||
}
|
||||
|
||||
private function readLine()
|
||||
{
|
||||
$pos = strpos($this->incomingBuffer, "\r\n", $this->incomingOffset);
|
||||
|
||||
if ($pos === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$ret = (string)substr($this->incomingBuffer, $this->incomingOffset, $pos - $this->incomingOffset);
|
||||
$this->incomingOffset = $pos + 2;
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
private function readLength($len)
|
||||
{
|
||||
$ret = substr($this->incomingBuffer, $this->incomingOffset, $len);
|
||||
if (strlen($ret) !== $len) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->incomingOffset += $len;
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* try to parse response from incoming buffer
|
||||
*
|
||||
* ripped from jdp/redisent, with some minor modifications to read from
|
||||
* the incoming buffer instead of issuing a blocking fread on a stream
|
||||
*
|
||||
* @throws ParserException if the incoming buffer is invalid
|
||||
* @return ModelInterface|null
|
||||
* @link https://github.com/jdp/redisent
|
||||
*/
|
||||
private function readResponse()
|
||||
{
|
||||
/* Parse the response based on the reply identifier */
|
||||
$reply = $this->readLine();
|
||||
if ($reply === null) {
|
||||
return null;
|
||||
}
|
||||
switch (substr($reply, 0, 1)) {
|
||||
/* Error reply */
|
||||
case '-':
|
||||
$response = new ErrorReply(substr($reply, 1));
|
||||
break;
|
||||
/* Inline reply */
|
||||
case '+':
|
||||
$response = new StatusReply(substr($reply, 1));
|
||||
break;
|
||||
/* Bulk reply */
|
||||
case '$':
|
||||
$size = (int)substr($reply, 1);
|
||||
if ($size === -1) {
|
||||
return new BulkReply(null);
|
||||
}
|
||||
$data = $this->readLength($size);
|
||||
if ($data === null) {
|
||||
return null;
|
||||
}
|
||||
if ($this->readLength(2) === null) { /* discard crlf */
|
||||
return null;
|
||||
}
|
||||
$response = new BulkReply($data);
|
||||
break;
|
||||
/* Multi-bulk reply */
|
||||
case '*':
|
||||
$count = (int)substr($reply, 1);
|
||||
if ($count === -1) {
|
||||
return new MultiBulkReply(null);
|
||||
}
|
||||
$response = array();
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$sub = $this->readResponse();
|
||||
if ($sub === null) {
|
||||
return null;
|
||||
}
|
||||
$response []= $sub;
|
||||
}
|
||||
$response = new MultiBulkReply($response);
|
||||
break;
|
||||
/* Integer reply */
|
||||
case ':':
|
||||
$response = new IntegerReply(substr($reply, 1));
|
||||
break;
|
||||
default:
|
||||
throw new ParserException('Invalid message can not be parsed: "' . $reply . '"');
|
||||
break;
|
||||
}
|
||||
/* Party on */
|
||||
return $response;
|
||||
}
|
||||
}
|
111
vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Serializer/RecursiveSerializer.php
vendored
Normal file
111
vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Serializer/RecursiveSerializer.php
vendored
Normal file
|
@ -0,0 +1,111 @@
|
|||
<?php
|
||||
|
||||
namespace Clue\Redis\Protocol\Serializer;
|
||||
|
||||
use Clue\Redis\Protocol\Model\StatusReply;
|
||||
use InvalidArgumentException;
|
||||
use Exception;
|
||||
use Clue\Redis\Protocol\Model\BulkReply;
|
||||
use Clue\Redis\Protocol\Model\IntegerReply;
|
||||
use Clue\Redis\Protocol\Model\ErrorReply;
|
||||
use Clue\Redis\Protocol\Model\MultiBulkReply;
|
||||
use Clue\Redis\Protocol\Model\ModelInterface;
|
||||
use Clue\Redis\Protocol\Model\Request;
|
||||
|
||||
class RecursiveSerializer implements SerializerInterface
|
||||
{
|
||||
const CRLF = "\r\n";
|
||||
|
||||
public function getRequestMessage($command, array $args = array())
|
||||
{
|
||||
$data = '*' . (count($args) + 1) . "\r\n$" . strlen($command) . "\r\n" . $command . "\r\n";
|
||||
foreach ($args as $arg) {
|
||||
$data .= '$' . strlen($arg) . "\r\n" . $arg . "\r\n";
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function createRequestModel($command, array $args = array())
|
||||
{
|
||||
return new Request($command, $args);
|
||||
}
|
||||
|
||||
public function getReplyMessage($data)
|
||||
{
|
||||
if (is_string($data) || $data === null) {
|
||||
return $this->getBulkMessage($data);
|
||||
} else if (is_int($data) || is_float($data) || is_bool($data)) {
|
||||
return $this->getIntegerMessage($data);
|
||||
} else if ($data instanceof Exception) {
|
||||
return $this->getErrorMessage($data->getMessage());
|
||||
} else if (is_array($data)) {
|
||||
return $this->getMultiBulkMessage($data);
|
||||
} else {
|
||||
throw new InvalidArgumentException('Invalid data type passed for serialization');
|
||||
}
|
||||
}
|
||||
|
||||
public function createReplyModel($data)
|
||||
{
|
||||
if (is_string($data) || $data === null) {
|
||||
return new BulkReply($data);
|
||||
} else if (is_int($data) || is_float($data) || is_bool($data)) {
|
||||
return new IntegerReply($data);
|
||||
} else if ($data instanceof Exception) {
|
||||
return new ErrorReply($data->getMessage());
|
||||
} else if (is_array($data)) {
|
||||
$models = array();
|
||||
foreach ($data as $one) {
|
||||
$models []= $this->createReplyModel($one);
|
||||
}
|
||||
return new MultiBulkReply($models);
|
||||
} else {
|
||||
throw new InvalidArgumentException('Invalid data type passed for serialization');
|
||||
}
|
||||
}
|
||||
|
||||
public function getBulkMessage($data)
|
||||
{
|
||||
if ($data === null) {
|
||||
/* null bulk reply */
|
||||
return '$-1' . self::CRLF;
|
||||
}
|
||||
/* bulk reply */
|
||||
return '$' . strlen($data) . self::CRLF . $data . self::CRLF;
|
||||
}
|
||||
|
||||
public function getErrorMessage($data)
|
||||
{
|
||||
/* error status reply */
|
||||
return '-' . $data . self::CRLF;
|
||||
}
|
||||
|
||||
public function getIntegerMessage($data)
|
||||
{
|
||||
return ':' . (int)$data . self::CRLF;
|
||||
}
|
||||
|
||||
public function getMultiBulkMessage($data)
|
||||
{
|
||||
if ($data === null) {
|
||||
/* null multi bulk reply */
|
||||
return '*-1' . self::CRLF;
|
||||
}
|
||||
/* multi bulk reply */
|
||||
$ret = '*' . count($data) . self::CRLF;
|
||||
foreach ($data as $one) {
|
||||
if ($one instanceof ModelInterface) {
|
||||
$ret .= $one->getMessageSerialized($this);
|
||||
} else {
|
||||
$ret .= $this->getReplyMessage($one);
|
||||
}
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
|
||||
public function getStatusMessage($data)
|
||||
{
|
||||
/* status reply */
|
||||
return '+' . $data . self::CRLF;
|
||||
}
|
||||
}
|
83
vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Serializer/SerializerInterface.php
vendored
Normal file
83
vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Serializer/SerializerInterface.php
vendored
Normal file
|
@ -0,0 +1,83 @@
|
|||
<?php
|
||||
|
||||
namespace Clue\Redis\Protocol\Serializer;
|
||||
|
||||
use Clue\Redis\Protocol\Model\ErrorReplyException;
|
||||
use Clue\Redis\Protocol\Model\ModelInterface;
|
||||
use Clue\Redis\Protocol\Model\MultiBulkReply;
|
||||
|
||||
interface SerializerInterface
|
||||
{
|
||||
/**
|
||||
* create a serialized unified request protocol message
|
||||
*
|
||||
* This is the *one* method most redis client libraries will likely want to
|
||||
* use in order to send a serialized message (a request) over the* wire to
|
||||
* your redis server instance.
|
||||
*
|
||||
* This method should be used in favor of constructing a request model and
|
||||
* then serializing it. While its effect might be equivalent, this method
|
||||
* is likely to (i.e. it /could/) provide a faster implementation.
|
||||
*
|
||||
* @param string $command
|
||||
* @param array $args
|
||||
* @return string
|
||||
* @see self::createRequestMessage()
|
||||
*/
|
||||
public function getRequestMessage($command, array $args = array());
|
||||
|
||||
/**
|
||||
* create a unified request protocol message model
|
||||
*
|
||||
* @param string $command
|
||||
* @param array $args
|
||||
* @return MultiBulkReply
|
||||
*/
|
||||
public function createRequestModel($command, array $args = array());
|
||||
|
||||
/**
|
||||
* create a serialized unified protocol reply message
|
||||
*
|
||||
* This is most useful for a redis server implementation which needs to
|
||||
* process client requests and send resulting reply messages.
|
||||
*
|
||||
* This method does its best to guess to right reply type and then returns
|
||||
* a serialized version of the message. It follows the "redis to lua
|
||||
* conversion table" (see link) which means most basic types can be mapped
|
||||
* as is.
|
||||
*
|
||||
* This method should be used in favor of constructing a reply model and
|
||||
* then serializing it. While its effect might be equivalent, this method
|
||||
* is likely to (i.e. it /could/) provide a faster implementation.
|
||||
*
|
||||
* Note however, you may still want to explicitly create a nested reply
|
||||
* model hierarchy if you need more control over the serialized message. For
|
||||
* instance, a null value will always be returned as a Null-Bulk-Reply, so
|
||||
* there's no way to express a Null-Multi-Bulk-Reply, unless you construct
|
||||
* it explicitly.
|
||||
*
|
||||
* @param mixed $data
|
||||
* @return string
|
||||
* @see self::createReplyModel()
|
||||
* @link http://redis.io/commands/eval
|
||||
*/
|
||||
public function getReplyMessage($data);
|
||||
|
||||
/**
|
||||
* create response message by determining datatype from given argument
|
||||
*
|
||||
* @param mixed $data
|
||||
* @return ModelInterface
|
||||
*/
|
||||
public function createReplyModel($data);
|
||||
|
||||
public function getBulkMessage($data);
|
||||
|
||||
public function getErrorMessage($data);
|
||||
|
||||
public function getIntegerMessage($data);
|
||||
|
||||
public function getMultiBulkMessage($data);
|
||||
|
||||
public function getStatusMessage($data);
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013 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.
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"name": "clue/redis-react",
|
||||
"description": "Async Redis client implementation, built on top of ReactPHP.",
|
||||
"keywords": ["Redis", "database", "client", "async", "ReactPHP"],
|
||||
"homepage": "https://github.com/clue/reactphp-redis",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Christian Lück",
|
||||
"email": "christian@clue.engineering"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=5.3",
|
||||
"clue/redis-protocol": "0.3.*",
|
||||
"evenement/evenement": "^3.0 || ^2.0 || ^1.0",
|
||||
"react/event-loop": "^1.2",
|
||||
"react/promise": "^2.0 || ^1.1",
|
||||
"react/promise-timer": "^1.8",
|
||||
"react/socket": "^1.9"
|
||||
},
|
||||
"require-dev": {
|
||||
"clue/block-react": "^1.1",
|
||||
"phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": { "Clue\\React\\Redis\\": "src/" }
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": { "Clue\\Tests\\React\\Redis\\": "tests/" }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
|
||||
namespace Clue\React\Redis;
|
||||
|
||||
use Evenement\EventEmitterInterface;
|
||||
use React\Promise\PromiseInterface;
|
||||
|
||||
/**
|
||||
* Simple interface for executing redis commands
|
||||
*
|
||||
* @event error(Exception $error)
|
||||
* @event close()
|
||||
*
|
||||
* @event message($channel, $message)
|
||||
* @event subscribe($channel, $numberOfChannels)
|
||||
* @event unsubscribe($channel, $numberOfChannels)
|
||||
*
|
||||
* @event pmessage($pattern, $channel, $message)
|
||||
* @event psubscribe($channel, $numberOfChannels)
|
||||
* @event punsubscribe($channel, $numberOfChannels)
|
||||
*/
|
||||
interface Client extends EventEmitterInterface
|
||||
{
|
||||
/**
|
||||
* Invoke the given command and return a Promise that will be fulfilled when the request has been replied to
|
||||
*
|
||||
* This is a magic method that will be invoked when calling any redis
|
||||
* command on this instance.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string[] $args
|
||||
* @return PromiseInterface Promise<mixed,Exception>
|
||||
*/
|
||||
public function __call($name, $args);
|
||||
|
||||
/**
|
||||
* end connection once all pending requests have been replied to
|
||||
*
|
||||
* @return void
|
||||
* @uses self::close() once all replies have been received
|
||||
* @see self::close() for closing the connection immediately
|
||||
*/
|
||||
public function end();
|
||||
|
||||
/**
|
||||
* close connection immediately
|
||||
*
|
||||
* This will emit the "close" event.
|
||||
*
|
||||
* @return void
|
||||
* @see self::end() for closing the connection once the client is idle
|
||||
*/
|
||||
public function close();
|
||||
}
|
|
@ -0,0 +1,191 @@
|
|||
<?php
|
||||
|
||||
namespace Clue\React\Redis;
|
||||
|
||||
use Clue\Redis\Protocol\Factory as ProtocolFactory;
|
||||
use React\EventLoop\Loop;
|
||||
use React\EventLoop\LoopInterface;
|
||||
use React\Promise\Deferred;
|
||||
use React\Promise\Timer\TimeoutException;
|
||||
use React\Socket\ConnectionInterface;
|
||||
use React\Socket\Connector;
|
||||
use React\Socket\ConnectorInterface;
|
||||
|
||||
class Factory
|
||||
{
|
||||
/** @var LoopInterface */
|
||||
private $loop;
|
||||
|
||||
/** @var ConnectorInterface */
|
||||
private $connector;
|
||||
|
||||
/** @var ProtocolFactory */
|
||||
private $protocol;
|
||||
|
||||
/**
|
||||
* @param ?LoopInterface $loop
|
||||
* @param ?ConnectorInterface $connector
|
||||
* @param ?ProtocolFactory $protocol
|
||||
*/
|
||||
public function __construct(LoopInterface $loop = null, ConnectorInterface $connector = null, ProtocolFactory $protocol = null)
|
||||
{
|
||||
$this->loop = $loop ?: Loop::get();
|
||||
$this->connector = $connector ?: new Connector(array(), $this->loop);
|
||||
$this->protocol = $protocol ?: new ProtocolFactory();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Redis client connected to address of given redis instance
|
||||
*
|
||||
* @param string $uri Redis server URI to connect to
|
||||
* @return \React\Promise\PromiseInterface<Client,\Exception> Promise that will
|
||||
* be fulfilled with `Client` on success or rejects with `\Exception` on error.
|
||||
*/
|
||||
public function createClient($uri)
|
||||
{
|
||||
// support `redis+unix://` scheme for Unix domain socket (UDS) paths
|
||||
if (preg_match('/^(redis\+unix:\/\/(?:[^:]*:[^@]*@)?)(.+?)?$/', $uri, $match)) {
|
||||
$parts = parse_url($match[1] . 'localhost/' . $match[2]);
|
||||
} else {
|
||||
if (strpos($uri, '://') === false) {
|
||||
$uri = 'redis://' . $uri;
|
||||
}
|
||||
|
||||
$parts = parse_url($uri);
|
||||
}
|
||||
|
||||
$uri = preg_replace(array('/(:)[^:\/]*(@)/', '/([?&]password=).*?($|&)/'), '$1***$2', $uri);
|
||||
if ($parts === false || !isset($parts['scheme'], $parts['host']) || !in_array($parts['scheme'], array('redis', 'rediss', 'redis+unix'))) {
|
||||
return \React\Promise\reject(new \InvalidArgumentException(
|
||||
'Invalid Redis URI given (EINVAL)',
|
||||
defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22
|
||||
));
|
||||
}
|
||||
|
||||
$args = array();
|
||||
parse_str(isset($parts['query']) ? $parts['query'] : '', $args);
|
||||
|
||||
$authority = $parts['host'] . ':' . (isset($parts['port']) ? $parts['port'] : 6379);
|
||||
if ($parts['scheme'] === 'rediss') {
|
||||
$authority = 'tls://' . $authority;
|
||||
} elseif ($parts['scheme'] === 'redis+unix') {
|
||||
$authority = 'unix://' . substr($parts['path'], 1);
|
||||
unset($parts['path']);
|
||||
}
|
||||
$connecting = $this->connector->connect($authority);
|
||||
|
||||
$deferred = new Deferred(function ($_, $reject) use ($connecting, $uri) {
|
||||
// connection cancelled, start with rejecting attempt, then clean up
|
||||
$reject(new \RuntimeException(
|
||||
'Connection to ' . $uri . ' cancelled (ECONNABORTED)',
|
||||
defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103
|
||||
));
|
||||
|
||||
// either close successful connection or cancel pending connection attempt
|
||||
$connecting->then(function (ConnectionInterface $connection) {
|
||||
$connection->close();
|
||||
});
|
||||
$connecting->cancel();
|
||||
});
|
||||
|
||||
$protocol = $this->protocol;
|
||||
$promise = $connecting->then(function (ConnectionInterface $stream) use ($protocol) {
|
||||
return new StreamingClient($stream, $protocol->createResponseParser(), $protocol->createSerializer());
|
||||
}, function (\Exception $e) use ($uri) {
|
||||
throw new \RuntimeException(
|
||||
'Connection to ' . $uri . ' failed: ' . $e->getMessage(),
|
||||
$e->getCode(),
|
||||
$e
|
||||
);
|
||||
});
|
||||
|
||||
// use `?password=secret` query or `user:secret@host` password form URL
|
||||
$pass = isset($args['password']) ? $args['password'] : (isset($parts['pass']) ? rawurldecode($parts['pass']) : null);
|
||||
if (isset($args['password']) || isset($parts['pass'])) {
|
||||
$pass = isset($args['password']) ? $args['password'] : rawurldecode($parts['pass']);
|
||||
$promise = $promise->then(function (StreamingClient $redis) use ($pass, $uri) {
|
||||
return $redis->auth($pass)->then(
|
||||
function () use ($redis) {
|
||||
return $redis;
|
||||
},
|
||||
function (\Exception $e) use ($redis, $uri) {
|
||||
$redis->close();
|
||||
|
||||
$const = '';
|
||||
$errno = $e->getCode();
|
||||
if ($errno === 0) {
|
||||
$const = ' (EACCES)';
|
||||
$errno = $e->getCode() ?: (defined('SOCKET_EACCES') ? SOCKET_EACCES : 13);
|
||||
}
|
||||
|
||||
throw new \RuntimeException(
|
||||
'Connection to ' . $uri . ' failed during AUTH command: ' . $e->getMessage() . $const,
|
||||
$errno,
|
||||
$e
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// use `?db=1` query or `/1` path (skip first slash)
|
||||
if (isset($args['db']) || (isset($parts['path']) && $parts['path'] !== '/')) {
|
||||
$db = isset($args['db']) ? $args['db'] : substr($parts['path'], 1);
|
||||
$promise = $promise->then(function (StreamingClient $redis) use ($db, $uri) {
|
||||
return $redis->select($db)->then(
|
||||
function () use ($redis) {
|
||||
return $redis;
|
||||
},
|
||||
function (\Exception $e) use ($redis, $uri) {
|
||||
$redis->close();
|
||||
|
||||
$const = '';
|
||||
$errno = $e->getCode();
|
||||
if ($errno === 0 && strpos($e->getMessage(), 'NOAUTH ') === 0) {
|
||||
$const = ' (EACCES)';
|
||||
$errno = defined('SOCKET_EACCES') ? SOCKET_EACCES : 13;
|
||||
} elseif ($errno === 0) {
|
||||
$const = ' (ENOENT)';
|
||||
$errno = defined('SOCKET_ENOENT') ? SOCKET_ENOENT : 2;
|
||||
}
|
||||
|
||||
throw new \RuntimeException(
|
||||
'Connection to ' . $uri . ' failed during SELECT command: ' . $e->getMessage() . $const,
|
||||
$errno,
|
||||
$e
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
$promise->then(array($deferred, 'resolve'), array($deferred, 'reject'));
|
||||
|
||||
// use timeout from explicit ?timeout=x parameter or default to PHP's default_socket_timeout (60)
|
||||
$timeout = isset($args['timeout']) ? (float) $args['timeout'] : (int) ini_get("default_socket_timeout");
|
||||
if ($timeout < 0) {
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
return \React\Promise\Timer\timeout($deferred->promise(), $timeout, $this->loop)->then(null, function ($e) use ($uri) {
|
||||
if ($e instanceof TimeoutException) {
|
||||
throw new \RuntimeException(
|
||||
'Connection to ' . $uri . ' timed out after ' . $e->getTimeout() . ' seconds (ETIMEDOUT)',
|
||||
defined('SOCKET_ETIMEDOUT') ? SOCKET_ETIMEDOUT : 110
|
||||
);
|
||||
}
|
||||
throw $e;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Redis client connected to address of given redis instance
|
||||
*
|
||||
* @param string $target
|
||||
* @return Client
|
||||
*/
|
||||
public function createLazyClient($target)
|
||||
{
|
||||
return new LazyClient($target, $this, $this->loop);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,219 @@
|
|||
<?php
|
||||
|
||||
namespace Clue\React\Redis;
|
||||
|
||||
use Evenement\EventEmitter;
|
||||
use React\Stream\Util;
|
||||
use React\EventLoop\LoopInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class LazyClient extends EventEmitter implements Client
|
||||
{
|
||||
private $target;
|
||||
/** @var Factory */
|
||||
private $factory;
|
||||
private $closed = false;
|
||||
private $promise;
|
||||
|
||||
private $loop;
|
||||
private $idlePeriod = 60.0;
|
||||
private $idleTimer;
|
||||
private $pending = 0;
|
||||
|
||||
private $subscribed = array();
|
||||
private $psubscribed = array();
|
||||
|
||||
/**
|
||||
* @param $target
|
||||
*/
|
||||
public function __construct($target, Factory $factory, LoopInterface $loop)
|
||||
{
|
||||
$args = array();
|
||||
\parse_str((string) \parse_url($target, \PHP_URL_QUERY), $args);
|
||||
if (isset($args['idle'])) {
|
||||
$this->idlePeriod = (float)$args['idle'];
|
||||
}
|
||||
|
||||
$this->target = $target;
|
||||
$this->factory = $factory;
|
||||
$this->loop = $loop;
|
||||
}
|
||||
|
||||
private function client()
|
||||
{
|
||||
if ($this->promise !== null) {
|
||||
return $this->promise;
|
||||
}
|
||||
|
||||
$self = $this;
|
||||
$pending =& $this->promise;
|
||||
$idleTimer=& $this->idleTimer;
|
||||
$subscribed =& $this->subscribed;
|
||||
$psubscribed =& $this->psubscribed;
|
||||
$loop = $this->loop;
|
||||
return $pending = $this->factory->createClient($this->target)->then(function (Client $redis) use ($self, &$pending, &$idleTimer, &$subscribed, &$psubscribed, $loop) {
|
||||
// connection completed => remember only until closed
|
||||
$redis->on('close', function () use (&$pending, $self, &$subscribed, &$psubscribed, &$idleTimer, $loop) {
|
||||
$pending = null;
|
||||
|
||||
// foward unsubscribe/punsubscribe events when underlying connection closes
|
||||
$n = count($subscribed);
|
||||
foreach ($subscribed as $channel => $_) {
|
||||
$self->emit('unsubscribe', array($channel, --$n));
|
||||
}
|
||||
$n = count($psubscribed);
|
||||
foreach ($psubscribed as $pattern => $_) {
|
||||
$self->emit('punsubscribe', array($pattern, --$n));
|
||||
}
|
||||
$subscribed = array();
|
||||
$psubscribed = array();
|
||||
|
||||
if ($idleTimer !== null) {
|
||||
$loop->cancelTimer($idleTimer);
|
||||
$idleTimer = null;
|
||||
}
|
||||
});
|
||||
|
||||
// keep track of all channels and patterns this connection is subscribed to
|
||||
$redis->on('subscribe', function ($channel) use (&$subscribed) {
|
||||
$subscribed[$channel] = true;
|
||||
});
|
||||
$redis->on('psubscribe', function ($pattern) use (&$psubscribed) {
|
||||
$psubscribed[$pattern] = true;
|
||||
});
|
||||
$redis->on('unsubscribe', function ($channel) use (&$subscribed) {
|
||||
unset($subscribed[$channel]);
|
||||
});
|
||||
$redis->on('punsubscribe', function ($pattern) use (&$psubscribed) {
|
||||
unset($psubscribed[$pattern]);
|
||||
});
|
||||
|
||||
Util::forwardEvents(
|
||||
$redis,
|
||||
$self,
|
||||
array(
|
||||
'message',
|
||||
'subscribe',
|
||||
'unsubscribe',
|
||||
'pmessage',
|
||||
'psubscribe',
|
||||
'punsubscribe',
|
||||
)
|
||||
);
|
||||
|
||||
return $redis;
|
||||
}, function (\Exception $e) use (&$pending) {
|
||||
// connection failed => discard connection attempt
|
||||
$pending = null;
|
||||
|
||||
throw $e;
|
||||
});
|
||||
}
|
||||
|
||||
public function __call($name, $args)
|
||||
{
|
||||
if ($this->closed) {
|
||||
return \React\Promise\reject(new \RuntimeException(
|
||||
'Connection closed (ENOTCONN)',
|
||||
defined('SOCKET_ENOTCONN') ? SOCKET_ENOTCONN : 107
|
||||
));
|
||||
}
|
||||
|
||||
$that = $this;
|
||||
return $this->client()->then(function (Client $redis) use ($name, $args, $that) {
|
||||
$that->awake();
|
||||
return \call_user_func_array(array($redis, $name), $args)->then(
|
||||
function ($result) use ($that) {
|
||||
$that->idle();
|
||||
return $result;
|
||||
},
|
||||
function ($error) use ($that) {
|
||||
$that->idle();
|
||||
throw $error;
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public function end()
|
||||
{
|
||||
if ($this->promise === null) {
|
||||
$this->close();
|
||||
}
|
||||
|
||||
if ($this->closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
$that = $this;
|
||||
return $this->client()->then(function (Client $redis) use ($that) {
|
||||
$redis->on('close', function () use ($that) {
|
||||
$that->close();
|
||||
});
|
||||
$redis->end();
|
||||
});
|
||||
}
|
||||
|
||||
public function close()
|
||||
{
|
||||
if ($this->closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->closed = true;
|
||||
|
||||
// either close active connection or cancel pending connection attempt
|
||||
if ($this->promise !== null) {
|
||||
$this->promise->then(function (Client $redis) {
|
||||
$redis->close();
|
||||
});
|
||||
if ($this->promise !== null) {
|
||||
$this->promise->cancel();
|
||||
$this->promise = null;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->idleTimer !== null) {
|
||||
$this->loop->cancelTimer($this->idleTimer);
|
||||
$this->idleTimer = null;
|
||||
}
|
||||
|
||||
$this->emit('close');
|
||||
$this->removeAllListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function awake()
|
||||
{
|
||||
++$this->pending;
|
||||
|
||||
if ($this->idleTimer !== null) {
|
||||
$this->loop->cancelTimer($this->idleTimer);
|
||||
$this->idleTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function idle()
|
||||
{
|
||||
--$this->pending;
|
||||
|
||||
if ($this->pending < 1 && $this->idlePeriod >= 0 && !$this->subscribed && !$this->psubscribed && $this->promise !== null) {
|
||||
$idleTimer =& $this->idleTimer;
|
||||
$promise =& $this->promise;
|
||||
$idleTimer = $this->loop->addTimer($this->idlePeriod, function () use (&$idleTimer, &$promise) {
|
||||
$promise->then(function (Client $redis) {
|
||||
$redis->close();
|
||||
});
|
||||
$promise = null;
|
||||
$idleTimer = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,203 @@
|
|||
<?php
|
||||
|
||||
namespace Clue\React\Redis;
|
||||
|
||||
use Clue\Redis\Protocol\Factory as ProtocolFactory;
|
||||
use Clue\Redis\Protocol\Model\ErrorReply;
|
||||
use Clue\Redis\Protocol\Model\ModelInterface;
|
||||
use Clue\Redis\Protocol\Model\MultiBulkReply;
|
||||
use Clue\Redis\Protocol\Parser\ParserException;
|
||||
use Clue\Redis\Protocol\Parser\ParserInterface;
|
||||
use Clue\Redis\Protocol\Serializer\SerializerInterface;
|
||||
use Evenement\EventEmitter;
|
||||
use React\Promise\Deferred;
|
||||
use React\Stream\DuplexStreamInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class StreamingClient extends EventEmitter implements Client
|
||||
{
|
||||
private $stream;
|
||||
private $parser;
|
||||
private $serializer;
|
||||
private $requests = array();
|
||||
private $ending = false;
|
||||
private $closed = false;
|
||||
|
||||
private $subscribed = 0;
|
||||
private $psubscribed = 0;
|
||||
|
||||
public function __construct(DuplexStreamInterface $stream, ParserInterface $parser = null, SerializerInterface $serializer = null)
|
||||
{
|
||||
if ($parser === null || $serializer === null) {
|
||||
$factory = new ProtocolFactory();
|
||||
if ($parser === null) {
|
||||
$parser = $factory->createResponseParser();
|
||||
}
|
||||
if ($serializer === null) {
|
||||
$serializer = $factory->createSerializer();
|
||||
}
|
||||
}
|
||||
|
||||
$that = $this;
|
||||
$stream->on('data', function($chunk) use ($parser, $that) {
|
||||
try {
|
||||
$models = $parser->pushIncoming($chunk);
|
||||
} catch (ParserException $error) {
|
||||
$that->emit('error', array(new \UnexpectedValueException(
|
||||
'Invalid data received: ' . $error->getMessage() . ' (EBADMSG)',
|
||||
defined('SOCKET_EBADMSG') ? SOCKET_EBADMSG : 77,
|
||||
$error
|
||||
)));
|
||||
$that->close();
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($models as $data) {
|
||||
try {
|
||||
$that->handleMessage($data);
|
||||
} catch (\UnderflowException $error) {
|
||||
$that->emit('error', array($error));
|
||||
$that->close();
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$stream->on('close', array($this, 'close'));
|
||||
|
||||
$this->stream = $stream;
|
||||
$this->parser = $parser;
|
||||
$this->serializer = $serializer;
|
||||
}
|
||||
|
||||
public function __call($name, $args)
|
||||
{
|
||||
$request = new Deferred();
|
||||
$promise = $request->promise();
|
||||
|
||||
$name = strtolower($name);
|
||||
|
||||
// special (p)(un)subscribe commands only accept a single parameter and have custom response logic applied
|
||||
static $pubsubs = array('subscribe', 'unsubscribe', 'psubscribe', 'punsubscribe');
|
||||
|
||||
if ($this->ending) {
|
||||
$request->reject(new \RuntimeException(
|
||||
'Connection ' . ($this->closed ? 'closed' : 'closing'). ' (ENOTCONN)',
|
||||
defined('SOCKET_ENOTCONN') ? SOCKET_ENOTCONN : 107
|
||||
));
|
||||
} elseif (count($args) !== 1 && in_array($name, $pubsubs)) {
|
||||
$request->reject(new \InvalidArgumentException(
|
||||
'PubSub commands limited to single argument (EINVAL)',
|
||||
defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22
|
||||
));
|
||||
} elseif ($name === 'monitor') {
|
||||
$request->reject(new \BadMethodCallException(
|
||||
'MONITOR command explicitly not supported (ENOTSUP)',
|
||||
defined('SOCKET_ENOTSUP') ? SOCKET_ENOTSUP : (defined('SOCKET_EOPNOTSUPP') ? SOCKET_EOPNOTSUPP : 95)
|
||||
));
|
||||
} else {
|
||||
$this->stream->write($this->serializer->getRequestMessage($name, $args));
|
||||
$this->requests []= $request;
|
||||
}
|
||||
|
||||
if (in_array($name, $pubsubs)) {
|
||||
$that = $this;
|
||||
$subscribed =& $this->subscribed;
|
||||
$psubscribed =& $this->psubscribed;
|
||||
|
||||
$promise->then(function ($array) use ($that, &$subscribed, &$psubscribed) {
|
||||
$first = array_shift($array);
|
||||
|
||||
// (p)(un)subscribe messages are to be forwarded
|
||||
$that->emit($first, $array);
|
||||
|
||||
// remember number of (p)subscribe topics
|
||||
if ($first === 'subscribe' || $first === 'unsubscribe') {
|
||||
$subscribed = $array[1];
|
||||
} else {
|
||||
$psubscribed = $array[1];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return $promise;
|
||||
}
|
||||
|
||||
public function handleMessage(ModelInterface $message)
|
||||
{
|
||||
if (($this->subscribed !== 0 || $this->psubscribed !== 0) && $message instanceof MultiBulkReply) {
|
||||
$array = $message->getValueNative();
|
||||
$first = array_shift($array);
|
||||
|
||||
// pub/sub messages are to be forwarded and should not be processed as request responses
|
||||
if (in_array($first, array('message', 'pmessage'))) {
|
||||
$this->emit($first, $array);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$this->requests) {
|
||||
throw new \UnderflowException(
|
||||
'Unexpected reply received, no matching request found (ENOMSG)',
|
||||
defined('SOCKET_ENOMSG') ? SOCKET_ENOMSG : 42
|
||||
);
|
||||
}
|
||||
|
||||
$request = array_shift($this->requests);
|
||||
assert($request instanceof Deferred);
|
||||
|
||||
if ($message instanceof ErrorReply) {
|
||||
$request->reject($message);
|
||||
} else {
|
||||
$request->resolve($message->getValueNative());
|
||||
}
|
||||
|
||||
if ($this->ending && !$this->requests) {
|
||||
$this->close();
|
||||
}
|
||||
}
|
||||
|
||||
public function end()
|
||||
{
|
||||
$this->ending = true;
|
||||
|
||||
if (!$this->requests) {
|
||||
$this->close();
|
||||
}
|
||||
}
|
||||
|
||||
public function close()
|
||||
{
|
||||
if ($this->closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->ending = true;
|
||||
$this->closed = true;
|
||||
|
||||
$remoteClosed = $this->stream->isReadable() === false && $this->stream->isWritable() === false;
|
||||
$this->stream->close();
|
||||
|
||||
$this->emit('close');
|
||||
|
||||
// reject all remaining requests in the queue
|
||||
while ($this->requests) {
|
||||
$request = array_shift($this->requests);
|
||||
assert($request instanceof Deferred);
|
||||
|
||||
if ($remoteClosed) {
|
||||
$request->reject(new \RuntimeException(
|
||||
'Connection closed by peer (ECONNRESET)',
|
||||
defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 104
|
||||
));
|
||||
} else {
|
||||
$request->reject(new \RuntimeException(
|
||||
'Connection closing (ECONNABORTED)',
|
||||
defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 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.
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"name": "clue/soap-react",
|
||||
"description": "Simple, async SOAP webservice client library, built on top of ReactPHP",
|
||||
"keywords": ["SOAP", "SoapClient", "WebService", "WSDL", "ReactPHP"],
|
||||
"homepage": "https://github.com/clue/reactphp-soap",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Christian Lück",
|
||||
"email": "christian@lueck.tv"
|
||||
}
|
||||
],
|
||||
"autoload": {
|
||||
"psr-4": { "Clue\\React\\Soap\\": "src/" }
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3",
|
||||
"clue/buzz-react": "^2.5",
|
||||
"react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3",
|
||||
"react/promise": "^2.1 || ^1.2",
|
||||
"ext-soap": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"clue/block-react": "^1.0",
|
||||
"phpunit/phpunit": "^6.4 || ^5.7 || ^4.8.35"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,323 @@
|
|||
<?php
|
||||
|
||||
namespace Clue\React\Soap;
|
||||
|
||||
use Clue\React\Buzz\Browser;
|
||||
use Clue\React\Soap\Protocol\ClientDecoder;
|
||||
use Clue\React\Soap\Protocol\ClientEncoder;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use React\Promise\Deferred;
|
||||
use React\Promise\PromiseInterface;
|
||||
|
||||
/**
|
||||
* The `Client` class is responsible for communication with the remote SOAP
|
||||
* WebService server.
|
||||
*
|
||||
* It requires a [`Browser`](https://github.com/clue/reactphp-buzz#browser) object
|
||||
* bound to the main [`EventLoop`](https://github.com/reactphp/event-loop#usage)
|
||||
* in order to handle async requests, the WSDL file contents and an optional
|
||||
* array of SOAP options:
|
||||
*
|
||||
* ```php
|
||||
* $loop = React\EventLoop\Factory::create();
|
||||
* $browser = new Clue\React\Buzz\Browser($loop);
|
||||
*
|
||||
* $wsdl = '<?xml …';
|
||||
* $options = array();
|
||||
*
|
||||
* $client = new Client($browser, $wsdl, $options);
|
||||
* ```
|
||||
*
|
||||
* If you need custom connector settings (DNS resolution, TLS parameters, timeouts,
|
||||
* proxy servers etc.), you can explicitly pass a custom instance of the
|
||||
* [`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface)
|
||||
* to the [`Browser`](https://github.com/clue/reactphp-buzz#browser) instance:
|
||||
*
|
||||
* ```php
|
||||
* $connector = new \React\Socket\Connector($loop, array(
|
||||
* 'dns' => '127.0.0.1',
|
||||
* 'tcp' => array(
|
||||
* 'bindto' => '192.168.10.1:0'
|
||||
* ),
|
||||
* 'tls' => array(
|
||||
* 'verify_peer' => false,
|
||||
* 'verify_peer_name' => false
|
||||
* )
|
||||
* ));
|
||||
*
|
||||
* $browser = new Browser($loop, $connector);
|
||||
* $client = new Client($browser, $wsdl);
|
||||
* ```
|
||||
*
|
||||
* The `Client` works similar to PHP's `SoapClient` (which it uses under the
|
||||
* hood), but leaves you the responsibility to load the WSDL file. This allows
|
||||
* you to use local WSDL files, WSDL files from a cache or the most common form,
|
||||
* downloading the WSDL file contents from an URL through the `Browser`:
|
||||
*
|
||||
* ```php
|
||||
* $browser = new Browser($loop);
|
||||
*
|
||||
* $browser->get($url)->then(
|
||||
* function (ResponseInterface $response) use ($browser) {
|
||||
* // WSDL file is ready, create client
|
||||
* $client = new Client($browser, (string)$response->getBody());
|
||||
*
|
||||
* // do something…
|
||||
* },
|
||||
* function (Exception $e) {
|
||||
* // an error occured while trying to download the WSDL
|
||||
* }
|
||||
* );
|
||||
* ```
|
||||
*
|
||||
* The `Client` constructor loads the given WSDL file contents into memory and
|
||||
* parses its definition. If the given WSDL file is invalid and can not be
|
||||
* parsed, this will throw a `SoapFault`:
|
||||
*
|
||||
* ```php
|
||||
* try {
|
||||
* $client = new Client($browser, $wsdl);
|
||||
* } catch (SoapFault $e) {
|
||||
* echo 'Error: ' . $e->getMessage() . PHP_EOL;
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* > Note that if you have `ext-xdebug` loaded, this may halt with a fatal
|
||||
* error instead of throwing a `SoapFault`. It is not recommended to use this
|
||||
* extension in production, so this should only ever affect test environments.
|
||||
*
|
||||
* The `Client` constructor accepts an array of options. All given options will
|
||||
* be passed through to the underlying `SoapClient`. However, not all options
|
||||
* make sense in this async implementation and as such may not have the desired
|
||||
* effect. See also [`SoapClient`](http://php.net/manual/en/soapclient.soapclient.php)
|
||||
* documentation for more details.
|
||||
*
|
||||
* If working in WSDL mode, the `$options` parameter is optional. If working in
|
||||
* non-WSDL mode, the WSDL parameter must be set to `null` and the options
|
||||
* parameter must contain the `location` and `uri` options, where `location` is
|
||||
* the URL of the SOAP server to send the request to, and `uri` is the target
|
||||
* namespace of the SOAP service:
|
||||
*
|
||||
* ```php
|
||||
* $client = new Client($browser, null, array(
|
||||
* 'location' => 'http://example.com',
|
||||
* 'uri' => 'http://ping.example.com',
|
||||
* ));
|
||||
* ```
|
||||
*
|
||||
* Similarly, if working in WSDL mode, the `location` option can be used to
|
||||
* explicitly overwrite the URL of the SOAP server to send the request to:
|
||||
*
|
||||
* ```php
|
||||
* $client = new Client($browser, $wsdl, array(
|
||||
* 'location' => 'http://example.com'
|
||||
* ));
|
||||
* ```
|
||||
*
|
||||
* You can use the `soap_version` option to change from the default SOAP 1.1 to
|
||||
* use SOAP 1.2 instead:
|
||||
*
|
||||
* ```php
|
||||
* $client = new Client($browser, $wsdl, array(
|
||||
* 'soap_version' => SOAP_1_2
|
||||
* ));
|
||||
* ```
|
||||
*
|
||||
* You can use the `classmap` option to map certain WSDL types to PHP classes
|
||||
* like this:
|
||||
*
|
||||
* ```php
|
||||
* $client = new Client($browser, $wsdl, array(
|
||||
* 'classmap' => array(
|
||||
* 'getBankResponseType' => BankResponse::class
|
||||
* )
|
||||
* ));
|
||||
* ```
|
||||
*
|
||||
* The `proxy_host` option (and family) is not supported by this library. As an
|
||||
* alternative, you can configure the given `$browser` instance to use an
|
||||
* [HTTP proxy server](https://github.com/clue/reactphp-buzz#http-proxy).
|
||||
* If you find any other option is missing or not supported here, PRs are much
|
||||
* appreciated!
|
||||
*
|
||||
* All public methods of the `Client` are considered *advanced usage*.
|
||||
* If you want to call RPC functions, see below for the [`Proxy`](#proxy) class.
|
||||
*/
|
||||
class Client
|
||||
{
|
||||
private $browser;
|
||||
private $encoder;
|
||||
private $decoder;
|
||||
|
||||
/**
|
||||
* Instantiate a new SOAP client for the given WSDL contents.
|
||||
*
|
||||
* @param Browser $browser
|
||||
* @param string|null $wsdlContents
|
||||
* @param array $options
|
||||
*/
|
||||
public function __construct(Browser $browser, $wsdlContents, array $options = array())
|
||||
{
|
||||
$wsdl = $wsdlContents !== null ? 'data://text/plain;base64,' . base64_encode($wsdlContents) : null;
|
||||
|
||||
// Accept HTTP responses with error status codes as valid responses.
|
||||
// This is done in order to process these error responses through the normal SOAP decoder.
|
||||
// Additionally, we explicitly limit number of redirects to zero because following redirects makes little sense
|
||||
// because it transforms the POST request to a GET one and hence loses the SOAP request body.
|
||||
$browser = $browser->withOptions(array(
|
||||
'obeySuccessCode' => false,
|
||||
'followRedirects' => true,
|
||||
'maxRedirects' => 0
|
||||
));
|
||||
|
||||
$this->browser = $browser;
|
||||
$this->encoder = new ClientEncoder($wsdl, $options);
|
||||
$this->decoder = new ClientDecoder($wsdl, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue the given function to be sent via SOAP and wait for a response from the remote web service.
|
||||
*
|
||||
* ```php
|
||||
* // advanced usage, see Proxy for recommended alternative
|
||||
* $promise = $client->soapCall('ping', array('hello', 42));
|
||||
* ```
|
||||
*
|
||||
* Note: This is considered *advanced usage*, you may want to look into using the [`Proxy`](#proxy) instead.
|
||||
*
|
||||
* ```php
|
||||
* $proxy = new Proxy($client);
|
||||
* $promise = $proxy->ping('hello', 42);
|
||||
* ```
|
||||
*
|
||||
* @param string $name
|
||||
* @param mixed[] $args
|
||||
* @return PromiseInterface Returns a Promise<mixed, Exception>
|
||||
*/
|
||||
public function soapCall($name, $args)
|
||||
{
|
||||
try {
|
||||
$request = $this->encoder->encode($name, $args);
|
||||
} catch (\Exception $e) {
|
||||
$deferred = new Deferred();
|
||||
$deferred->reject($e);
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
$decoder = $this->decoder;
|
||||
|
||||
return $this->browser->send($request)->then(
|
||||
function (ResponseInterface $response) use ($decoder, $name) {
|
||||
// HTTP response received => decode results for this function call
|
||||
return $decoder->decode($name, (string)$response->getBody());
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of functions defined in the WSDL.
|
||||
*
|
||||
* It returns the equivalent of PHP's
|
||||
* [`SoapClient::__getFunctions()`](http://php.net/manual/en/soapclient.getfunctions.php).
|
||||
* In non-WSDL mode, this method returns `null`.
|
||||
*
|
||||
* @return string[]|null
|
||||
*/
|
||||
public function getFunctions()
|
||||
{
|
||||
return $this->encoder->__getFunctions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of types defined in the WSDL.
|
||||
*
|
||||
* It returns the equivalent of PHP's
|
||||
* [`SoapClient::__getTypes()`](http://php.net/manual/en/soapclient.gettypes.php).
|
||||
* In non-WSDL mode, this method returns `null`.
|
||||
*
|
||||
* @return string[]|null
|
||||
*/
|
||||
public function getTypes()
|
||||
{
|
||||
return $this->encoder->__getTypes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the location (URI) of the given webservice `$function`.
|
||||
*
|
||||
* Note that this is not to be confused with the WSDL file location.
|
||||
* A WSDL file can contain any number of function definitions.
|
||||
* It's very common that all of these functions use the same location definition.
|
||||
* However, technically each function can potentially use a different location.
|
||||
*
|
||||
* The `$function` parameter should be a string with the the SOAP function name.
|
||||
* See also [`getFunctions()`](#getfunctions) for a list of all available functions.
|
||||
*
|
||||
* ```php
|
||||
* assert('http://example.com/soap/service' === $client->getLocation('echo'));
|
||||
* ```
|
||||
*
|
||||
* For easier access, this function also accepts a numeric function index.
|
||||
* It then uses [`getFunctions()`](#getfunctions) internally to get the function
|
||||
* name for the given index.
|
||||
* This is particularly useful for the very common case where all functions use the
|
||||
* same location and accessing the first location is sufficient.
|
||||
*
|
||||
* ```php
|
||||
* assert('http://example.com/soap/service' === $client->getLocation(0));
|
||||
* ```
|
||||
*
|
||||
* When the `location` option has been set in the `Client` constructor
|
||||
* (such as when in non-WSDL mode) or via the `withLocation()` method, this
|
||||
* method returns the value of the given location.
|
||||
*
|
||||
* Passing a `$function` not defined in the WSDL file will throw a `SoapFault`.
|
||||
*
|
||||
* @param string|int $function
|
||||
* @return string
|
||||
* @throws \SoapFault if given function does not exist
|
||||
* @see self::getFunctions()
|
||||
*/
|
||||
public function getLocation($function)
|
||||
{
|
||||
if (is_int($function)) {
|
||||
$functions = $this->getFunctions();
|
||||
if (isset($functions[$function]) && preg_match('/^\w+ (\w+)\(/', $functions[$function], $match)) {
|
||||
$function = $match[1];
|
||||
}
|
||||
}
|
||||
|
||||
// encode request for given $function
|
||||
return (string)$this->encoder->encode($function, array())->getUri();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new `Client` with the updated location (URI) for all functions.
|
||||
*
|
||||
* Note that this is not to be confused with the WSDL file location.
|
||||
* A WSDL file can contain any number of function definitions.
|
||||
* It's very common that all of these functions use the same location definition.
|
||||
* However, technically each function can potentially use a different location.
|
||||
*
|
||||
* ```php
|
||||
* $client = $client->withLocation('http://example.com/soap');
|
||||
*
|
||||
* assert('http://example.com/soap' === $client->getLocation('echo'));
|
||||
* ```
|
||||
*
|
||||
* As an alternative to this method, you can also set the `location` option
|
||||
* in the `Client` constructor (such as when in non-WSDL mode).
|
||||
*
|
||||
* @param string $location
|
||||
* @return self
|
||||
* @see self::getLocation()
|
||||
*/
|
||||
public function withLocation($location)
|
||||
{
|
||||
$client = clone $this;
|
||||
$client->encoder = clone $this->encoder;
|
||||
$client->encoder->__setLocation($location);
|
||||
|
||||
return $client;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
|
||||
namespace Clue\React\Soap\Protocol;
|
||||
|
||||
use \SoapClient;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class ClientDecoder extends SoapClient
|
||||
{
|
||||
private $response = null;
|
||||
|
||||
/**
|
||||
* Decodes the SOAP response / return value from the given SOAP envelope (HTTP response body)
|
||||
*
|
||||
* @param string $function
|
||||
* @param string $response
|
||||
* @return mixed
|
||||
* @throws \SoapFault if response indicates a fault (error condition) or is invalid
|
||||
*/
|
||||
public function decode($function, $response)
|
||||
{
|
||||
// Temporarily save response internally for further processing
|
||||
$this->response = $response;
|
||||
|
||||
// Let's pretend we just invoked the given SOAP function.
|
||||
// This won't actually invoke anything (see `__doRequest()`), but this
|
||||
// requires a valid function name to match its definition in the WSDL.
|
||||
// Internally, simply use the injected response to parse its results.
|
||||
$ret = $this->__soapCall($function, array());
|
||||
$this->response = null;
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overwrites the internal request logic to parse the response
|
||||
*
|
||||
* By overwriting this method, we can skip the actual request sending logic
|
||||
* and still use the internal parsing logic by injecting the response as
|
||||
* the return code in this method. This will implicitly be invoked by the
|
||||
* call to `pseudoCall()` in the above `decode()` method.
|
||||
*
|
||||
* @see SoapClient::__doRequest()
|
||||
*/
|
||||
public function __doRequest($request, $location, $action, $version, $one_way = 0)
|
||||
{
|
||||
// the actual result doesn't actually matter, just return the given result
|
||||
// this will be processed internally and will return the parsed result
|
||||
return $this->response;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
|
||||
namespace Clue\React\Soap\Protocol;
|
||||
|
||||
use \SoapClient;
|
||||
use RingCentral\Psr7\Request;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class ClientEncoder extends SoapClient
|
||||
{
|
||||
private $request = null;
|
||||
|
||||
/**
|
||||
* Encodes the given RPC function name and arguments as a SOAP request
|
||||
*
|
||||
* @param string $name
|
||||
* @param array $args
|
||||
* @return Request
|
||||
* @throws \SoapFault if request is invalid according to WSDL
|
||||
*/
|
||||
public function encode($name, $args)
|
||||
{
|
||||
$this->__soapCall($name, $args);
|
||||
|
||||
$request = $this->request;
|
||||
$this->request = null;
|
||||
|
||||
return $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overwrites the internal request logic to build the request message
|
||||
*
|
||||
* By overwriting this method, we can skip the actual request sending logic
|
||||
* and still use the internal request serializing logic by accessing the
|
||||
* given `$request` parameter and building our custom request object from
|
||||
* it. We skip/ignore its parsing logic by returing an empty response here.
|
||||
* This will implicitly be invoked by the call to `__soapCall()` in the
|
||||
* above `encode()` method.
|
||||
*
|
||||
* @see SoapClient::__doRequest()
|
||||
*/
|
||||
public function __doRequest($request, $location, $action, $version, $one_way = 0)
|
||||
{
|
||||
$headers = array();
|
||||
if ($version === SOAP_1_1) {
|
||||
$headers = array(
|
||||
'SOAPAction' => $action,
|
||||
'Content-Type' => 'text/xml; charset=utf-8'
|
||||
);
|
||||
} elseif ($version === SOAP_1_2) {
|
||||
$headers = array(
|
||||
'Content-Type' => 'application/soap+xml; charset=utf-8; action=' . $action
|
||||
);
|
||||
}
|
||||
|
||||
$this->request = new Request(
|
||||
'POST',
|
||||
(string)$location,
|
||||
$headers,
|
||||
(string)$request
|
||||
);
|
||||
|
||||
// do not actually block here, just pretend we're done...
|
||||
return '';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
namespace Clue\React\Soap;
|
||||
|
||||
use React\Promise\PromiseInterface;
|
||||
|
||||
/**
|
||||
* The `Proxy` class wraps an existing [`Client`](#client) instance in order to ease calling
|
||||
* SOAP functions.
|
||||
*
|
||||
* ```php
|
||||
* $proxy = new Proxy($client);
|
||||
* ```
|
||||
*
|
||||
* Each and every method call to the `Proxy` class will be sent via SOAP.
|
||||
*
|
||||
* ```php
|
||||
* $proxy->myMethod($myArg1, $myArg2)->then(function ($response) {
|
||||
* // result received
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* Please refer to your WSDL or its accompanying documentation for details
|
||||
* on which functions and arguments are supported.
|
||||
*
|
||||
* > Note that this class is called "Proxy" because it will forward (proxy) all
|
||||
* method calls to the actual SOAP service via the underlying
|
||||
* [`Client::soapCall()`](#soapcall) method. This is not to be confused with
|
||||
* using a proxy server. See [`Client`](#client) documentation for more
|
||||
* details on how to use an HTTP proxy server.
|
||||
*/
|
||||
final class Proxy
|
||||
{
|
||||
private $client;
|
||||
|
||||
public function __construct(Client $client)
|
||||
{
|
||||
$this->client = $client;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param mixed[] $args
|
||||
* @return PromiseInterface
|
||||
*/
|
||||
public function __call($name, $args)
|
||||
{
|
||||
return $this->client->soapCall($name, $args);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013 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.
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"name": "clue/socket-raw",
|
||||
"description": "Simple and lightweight OOP wrapper for PHP's low-level sockets extension (ext-sockets).",
|
||||
"keywords": ["socket", "stream", "datagram", "dgram", "client", "server", "ipv6", "tcp", "udp", "icmp", "unix", "udg"],
|
||||
"homepage": "https://github.com/clue/socket-raw",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Christian Lück",
|
||||
"email": "christian@clue.engineering"
|
||||
}
|
||||
],
|
||||
"autoload": {
|
||||
"psr-4": {"Socket\\Raw\\": "src"}
|
||||
},
|
||||
"require": {
|
||||
"ext-sockets": "*",
|
||||
"php": ">=5.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
<?php
|
||||
|
||||
namespace Socket\Raw;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class Exception extends RuntimeException
|
||||
{
|
||||
/**
|
||||
* Create an Exception after a socket operation on the given $resource failed
|
||||
*
|
||||
* @param \Socket|resource $resource
|
||||
* @param string $messagePrefix
|
||||
* @return self
|
||||
* @uses socket_last_error() to get last socket error code
|
||||
* @uses socket_clear_error() to clear socket error code
|
||||
* @uses self::createFromCode() to automatically construct exception with full error message
|
||||
*/
|
||||
public static function createFromSocketResource($resource, $messagePrefix = 'Socket operation failed')
|
||||
{
|
||||
if (PHP_VERSION_ID >= 80000) {
|
||||
try {
|
||||
$code = socket_last_error($resource);
|
||||
} catch (\Error $e) {
|
||||
$code = SOCKET_ENOTSOCK;
|
||||
}
|
||||
} elseif (is_resource($resource)) {
|
||||
$code = socket_last_error($resource);
|
||||
socket_clear_error($resource);
|
||||
} else {
|
||||
// socket already closed, return fixed error code instead of operating on invalid handle
|
||||
$code = SOCKET_ENOTSOCK;
|
||||
}
|
||||
|
||||
return self::createFromCode($code, $messagePrefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an Exception after a global socket operation failed (like socket creation)
|
||||
*
|
||||
* @param string $messagePrefix
|
||||
* @return self
|
||||
* @uses socket_last_error() to get last global error code
|
||||
* @uses socket_clear_error() to clear global error code
|
||||
* @uses self::createFromCode() to automatically construct exception with full error message
|
||||
*/
|
||||
public static function createFromGlobalSocketOperation($messagePrefix = 'Socket operation failed')
|
||||
{
|
||||
$code = socket_last_error();
|
||||
socket_clear_error();
|
||||
|
||||
return self::createFromCode($code, $messagePrefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an Exception for given error $code
|
||||
*
|
||||
* @param int $code
|
||||
* @param string $messagePrefix
|
||||
* @return self
|
||||
* @throws Exception if given $val is boolean false
|
||||
* @uses self::getErrorMessage() to translate error code to error message
|
||||
*/
|
||||
public static function createFromCode($code, $messagePrefix = 'Socket error')
|
||||
{
|
||||
return new self($messagePrefix . ': ' . self::getErrorMessage($code), $code);
|
||||
}
|
||||
|
||||
/**
|
||||
* get error message for given error code
|
||||
*
|
||||
* @param int $code error code
|
||||
* @return string
|
||||
* @uses socket_strerror() to translate error code to error message
|
||||
* @uses get_defined_constants() to check for related error constant
|
||||
*/
|
||||
protected static function getErrorMessage($code)
|
||||
{
|
||||
$string = socket_strerror($code);
|
||||
|
||||
// search constant starting with SOCKET_ for this error code
|
||||
foreach (get_defined_constants() as $key => $value) {
|
||||
if($value === $code && strpos($key, 'SOCKET_') === 0) {
|
||||
$string .= ' (' . $key . ')';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $string;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,282 @@
|
|||
<?php
|
||||
|
||||
namespace Socket\Raw;
|
||||
|
||||
use \InvalidArgumentException;
|
||||
|
||||
class Factory
|
||||
{
|
||||
/**
|
||||
* create client socket connected to given target address
|
||||
*
|
||||
* @param string $address target address to connect to
|
||||
* @param null|float $timeout connection timeout (in seconds), default null = no limit
|
||||
* @return \Socket\Raw\Socket
|
||||
* @throws InvalidArgumentException if given address is invalid
|
||||
* @throws Exception on error
|
||||
* @uses self::createFromString()
|
||||
* @uses Socket::connect()
|
||||
* @uses Socket::connectTimeout()
|
||||
*/
|
||||
public function createClient($address, $timeout = null)
|
||||
{
|
||||
$socket = $this->createFromString($address, $scheme);
|
||||
|
||||
try {
|
||||
if ($timeout === null) {
|
||||
$socket->connect($address);
|
||||
} else {
|
||||
// connectTimeout enables non-blocking mode, so turn blocking on again
|
||||
$socket->connectTimeout($address, $timeout);
|
||||
$socket->setBlocking(true);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$socket->close();
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return $socket;
|
||||
}
|
||||
|
||||
/**
|
||||
* create server socket bound to given address (and start listening for streaming clients to connect to this stream socket)
|
||||
*
|
||||
* @param string $address address to bind socket to
|
||||
* @return \Socket\Raw\Socket
|
||||
* @throws Exception on error
|
||||
* @uses self::createFromString()
|
||||
* @uses Socket::bind()
|
||||
* @uses Socket::listen() only for stream sockets (TCP/UNIX)
|
||||
*/
|
||||
public function createServer($address)
|
||||
{
|
||||
$socket = $this->createFromString($address, $scheme);
|
||||
|
||||
try {
|
||||
$socket->bind($address);
|
||||
|
||||
if ($socket->getType() === SOCK_STREAM) {
|
||||
$socket->listen();
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$socket->close();
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return $socket;
|
||||
}
|
||||
|
||||
/**
|
||||
* create TCP/IPv4 stream socket
|
||||
*
|
||||
* @return \Socket\Raw\Socket
|
||||
* @throws Exception on error
|
||||
* @uses self::create()
|
||||
*/
|
||||
public function createTcp4()
|
||||
{
|
||||
return $this->create(AF_INET, SOCK_STREAM, SOL_TCP);
|
||||
}
|
||||
|
||||
/**
|
||||
* create TCP/IPv6 stream socket
|
||||
*
|
||||
* @return \Socket\Raw\Socket
|
||||
* @throws Exception on error
|
||||
* @uses self::create()
|
||||
*/
|
||||
public function createTcp6()
|
||||
{
|
||||
return $this->create(AF_INET6, SOCK_STREAM, SOL_TCP);
|
||||
}
|
||||
|
||||
/**
|
||||
* create UDP/IPv4 datagram socket
|
||||
*
|
||||
* @return \Socket\Raw\Socket
|
||||
* @throws Exception on error
|
||||
* @uses self::create()
|
||||
*/
|
||||
public function createUdp4()
|
||||
{
|
||||
return $this->create(AF_INET, SOCK_DGRAM, SOL_UDP);
|
||||
}
|
||||
|
||||
/**
|
||||
* create UDP/IPv6 datagram socket
|
||||
*
|
||||
* @return \Socket\Raw\Socket
|
||||
* @throws Exception on error
|
||||
* @uses self::create()
|
||||
*/
|
||||
public function createUdp6()
|
||||
{
|
||||
return $this->create(AF_INET6, SOCK_DGRAM, SOL_UDP);
|
||||
}
|
||||
|
||||
/**
|
||||
* create local UNIX stream socket
|
||||
*
|
||||
* @return \Socket\Raw\Socket
|
||||
* @throws Exception on error
|
||||
* @uses self::create()
|
||||
*/
|
||||
public function createUnix()
|
||||
{
|
||||
return $this->create(AF_UNIX, SOCK_STREAM, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* create local UNIX datagram socket (UDG)
|
||||
*
|
||||
* @return \Socket\Raw\Socket
|
||||
* @throws Exception on error
|
||||
* @uses self::create()
|
||||
*/
|
||||
public function createUdg()
|
||||
{
|
||||
return $this->create(AF_UNIX, SOCK_DGRAM, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* create raw ICMP/IPv4 datagram socket (requires root!)
|
||||
*
|
||||
* @return \Socket\Raw\Socket
|
||||
* @throws Exception on error
|
||||
* @uses self::create()
|
||||
*/
|
||||
public function createIcmp4()
|
||||
{
|
||||
return $this->create(AF_INET, SOCK_RAW, getprotobyname('icmp'));
|
||||
}
|
||||
|
||||
/**
|
||||
* create raw ICMPv6 (IPv6) datagram socket (requires root!)
|
||||
*
|
||||
* @return \Socket\Raw\Socket
|
||||
* @throws Exception on error
|
||||
* @uses self::create()
|
||||
*/
|
||||
public function createIcmp6()
|
||||
{
|
||||
return $this->create(AF_INET6, SOCK_RAW, 58 /*getprotobyname('icmp')*/);
|
||||
}
|
||||
|
||||
/**
|
||||
* create low level socket with given arguments
|
||||
*
|
||||
* @param int $domain
|
||||
* @param int $type
|
||||
* @param int $protocol
|
||||
* @return \Socket\Raw\Socket
|
||||
* @throws Exception if creating socket fails
|
||||
* @throws \Error PHP 8 only: throws \Error when arguments are invalid
|
||||
* @uses socket_create()
|
||||
*/
|
||||
public function create($domain, $type, $protocol)
|
||||
{
|
||||
$sock = @socket_create($domain, $type, $protocol);
|
||||
if ($sock === false) {
|
||||
throw Exception::createFromGlobalSocketOperation('Unable to create socket');
|
||||
}
|
||||
return new Socket($sock);
|
||||
}
|
||||
|
||||
/**
|
||||
* create a pair of indistinguishable sockets (commonly used in IPC)
|
||||
*
|
||||
* @param int $domain
|
||||
* @param int $type
|
||||
* @param int $protocol
|
||||
* @return \Socket\Raw\Socket[]
|
||||
* @throws Exception if creating pair of sockets fails
|
||||
* @throws \Error PHP 8 only: throws \Error when arguments are invalid
|
||||
* @uses socket_create_pair()
|
||||
*/
|
||||
public function createPair($domain, $type, $protocol)
|
||||
{
|
||||
$ret = @socket_create_pair($domain, $type, $protocol, $pair);
|
||||
if ($ret === false) {
|
||||
throw Exception::createFromGlobalSocketOperation('Unable to create pair of sockets');
|
||||
}
|
||||
return array(new Socket($pair[0]), new Socket($pair[1]));
|
||||
}
|
||||
|
||||
/**
|
||||
* create TCP/IPv4 stream socket and listen for new connections
|
||||
*
|
||||
* @param int $port
|
||||
* @param int $backlog
|
||||
* @return \Socket\Raw\Socket
|
||||
* @throws Exception if creating listening socket fails
|
||||
* @throws \Error PHP 8 only: throws \Error when arguments are invalid
|
||||
* @uses socket_create_listen()
|
||||
* @see self::createServer() as an alternative to bind to specific IP, IPv6, UDP, UNIX, UGP
|
||||
*/
|
||||
public function createListen($port, $backlog = 128)
|
||||
{
|
||||
$sock = @socket_create_listen($port, $backlog);
|
||||
if ($sock === false) {
|
||||
throw Exception::createFromGlobalSocketOperation('Unable to create listening socket');
|
||||
}
|
||||
return new Socket($sock);
|
||||
}
|
||||
|
||||
/**
|
||||
* create socket for given address
|
||||
*
|
||||
* @param string $address (passed by reference in order to remove scheme, if present)
|
||||
* @param string|null $scheme default scheme to use, defaults to TCP (passed by reference in order to update with actual scheme used)
|
||||
* @return \Socket\Raw\Socket
|
||||
* @throws InvalidArgumentException if given address is invalid
|
||||
* @throws Exception in case creating socket failed
|
||||
* @uses self::createTcp4() etc.
|
||||
*/
|
||||
public function createFromString(&$address, &$scheme)
|
||||
{
|
||||
if ($scheme === null) {
|
||||
$scheme = 'tcp';
|
||||
}
|
||||
|
||||
$hasScheme = false;
|
||||
|
||||
$pos = strpos($address, '://');
|
||||
if ($pos !== false) {
|
||||
$scheme = substr($address, 0, $pos);
|
||||
$address = substr($address, $pos + 3);
|
||||
$hasScheme = true;
|
||||
}
|
||||
|
||||
if (strpos($address, ':') !== strrpos($address, ':') && in_array($scheme, array('tcp', 'udp', 'icmp'))) {
|
||||
// TCP/UDP/ICMP address with several colons => must be IPv6
|
||||
$scheme .= '6';
|
||||
}
|
||||
|
||||
if ($scheme === 'tcp') {
|
||||
$socket = $this->createTcp4();
|
||||
} elseif ($scheme === 'udp') {
|
||||
$socket = $this->createUdp4();
|
||||
} elseif ($scheme === 'tcp6') {
|
||||
$socket = $this->createTcp6();
|
||||
} elseif ($scheme === 'udp6') {
|
||||
$socket = $this->createUdp6();
|
||||
} elseif ($scheme === 'unix') {
|
||||
$socket = $this->createUnix();
|
||||
} elseif ($scheme === 'udg') {
|
||||
$socket = $this->createUdg();
|
||||
} elseif ($scheme === 'icmp') {
|
||||
$socket = $this->createIcmp4();
|
||||
} elseif ($scheme === 'icmp6') {
|
||||
$socket = $this->createIcmp6();
|
||||
if ($hasScheme) {
|
||||
// scheme was stripped from address, resulting IPv6 must not
|
||||
// have a port (due to ICMP) and thus must not be enclosed in
|
||||
// square brackets
|
||||
$address = trim($address, '[]');
|
||||
}
|
||||
} else {
|
||||
throw new InvalidArgumentException('Invalid address scheme given');
|
||||
}
|
||||
return $socket;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,562 @@
|
|||
<?php
|
||||
|
||||
namespace Socket\Raw;
|
||||
|
||||
/**
|
||||
* Simple and lightweight OOP wrapper for the low-level sockets extension (ext-sockets)
|
||||
*
|
||||
* @author clue
|
||||
* @link https://github.com/clue/php-socket-raw
|
||||
*/
|
||||
class Socket
|
||||
{
|
||||
/**
|
||||
* reference to actual socket resource
|
||||
*
|
||||
* @var \Socket|resource
|
||||
*/
|
||||
private $resource;
|
||||
|
||||
/**
|
||||
* instanciate socket wrapper for given socket resource
|
||||
*
|
||||
* should usually not be called manually, see Factory
|
||||
*
|
||||
* @param \Socket|resource $resource
|
||||
* @see Factory as the preferred (and simplest) way to construct socket instances
|
||||
*/
|
||||
public function __construct($resource)
|
||||
{
|
||||
$this->resource = $resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* get actual socket resource
|
||||
*
|
||||
* @return \Socket|resource returns the socket resource (a `Socket` object as of PHP 8)
|
||||
*/
|
||||
public function getResource()
|
||||
{
|
||||
return $this->resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* accept an incomming connection on this listening socket
|
||||
*
|
||||
* @return \Socket\Raw\Socket new connected socket used for communication
|
||||
* @throws Exception on error, if this is not a listening socket or there's no connection pending
|
||||
* @throws \Error PHP 8 only: throws \Error when socket is invalid
|
||||
* @see self::selectRead() to check if this listening socket can accept()
|
||||
* @see Factory::createServer() to create a listening socket
|
||||
* @see self::listen() has to be called first
|
||||
* @uses socket_accept()
|
||||
*/
|
||||
public function accept()
|
||||
{
|
||||
$resource = @socket_accept($this->resource);
|
||||
if ($resource === false) {
|
||||
throw Exception::createFromGlobalSocketOperation();
|
||||
}
|
||||
return new Socket($resource);
|
||||
}
|
||||
|
||||
/**
|
||||
* binds a name/address/path to this socket
|
||||
*
|
||||
* has to be called before issuing connect() or listen()
|
||||
*
|
||||
* @param string $address either of IPv4:port, hostname:port, [IPv6]:port, unix-path
|
||||
* @return self $this (chainable)
|
||||
* @throws Exception on error
|
||||
* @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
|
||||
* @uses socket_bind()
|
||||
*/
|
||||
public function bind($address)
|
||||
{
|
||||
$ret = @socket_bind($this->resource, $this->unformatAddress($address, $port), $port);
|
||||
if ($ret === false) {
|
||||
throw Exception::createFromSocketResource($this->resource);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* close this socket
|
||||
*
|
||||
* ATTENTION: make sure to NOT re-use this socket instance after closing it!
|
||||
* its socket resource remains closed and most further operations will fail!
|
||||
*
|
||||
* @return self $this (chainable)
|
||||
* @throws \Error PHP 8 only: throws \Error when socket is invalid
|
||||
* @see self::shutdown() should be called before closing socket
|
||||
* @uses socket_close()
|
||||
*/
|
||||
public function close()
|
||||
{
|
||||
socket_close($this->resource);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* initiate a connection to given address
|
||||
*
|
||||
* @param string $address either of IPv4:port, hostname:port, [IPv6]:port, unix-path
|
||||
* @return self $this (chainable)
|
||||
* @throws Exception on error
|
||||
* @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
|
||||
* @uses socket_connect()
|
||||
*/
|
||||
public function connect($address)
|
||||
{
|
||||
$ret = @socket_connect($this->resource, $this->unformatAddress($address, $port), $port);
|
||||
if ($ret === false) {
|
||||
throw Exception::createFromSocketResource($this->resource);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates a new connection to given address, wait for up to $timeout seconds
|
||||
*
|
||||
* The given $timeout parameter is an upper bound, a maximum time to wait
|
||||
* for the connection to be either accepted or rejected.
|
||||
*
|
||||
* The resulting socket resource will be set to non-blocking mode,
|
||||
* regardless of its previous state and whether this method succedes or
|
||||
* if it fails. Make sure to reset with `setBlocking(true)` if you want to
|
||||
* continue using blocking calls.
|
||||
*
|
||||
* @param string $address either of IPv4:port, hostname:port, [IPv6]:port, unix-path
|
||||
* @param float $timeout maximum time to wait (in seconds)
|
||||
* @return self $this (chainable)
|
||||
* @throws Exception on error
|
||||
* @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
|
||||
* @uses self::setBlocking() to enable non-blocking mode
|
||||
* @uses self::connect() to initiate the connection
|
||||
* @uses self::selectWrite() to wait for the connection to complete
|
||||
* @uses self::assertAlive() to check connection state
|
||||
*/
|
||||
public function connectTimeout($address, $timeout)
|
||||
{
|
||||
$this->setBlocking(false);
|
||||
|
||||
try {
|
||||
// socket is non-blocking, so connect should emit EINPROGRESS
|
||||
$this->connect($address);
|
||||
|
||||
// socket is already connected immediately?
|
||||
return $this;
|
||||
} catch (Exception $e) {
|
||||
// non-blocking connect() should be EINPROGRESS (or EWOULDBLOCK on Windows) => otherwise re-throw
|
||||
if ($e->getCode() !== SOCKET_EINPROGRESS && $e->getCode() !== SOCKET_EWOULDBLOCK) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
// connection should be completed (or rejected) within timeout: socket becomes writable on success or error
|
||||
// Windows requires special care because it uses exceptfds for socket errors: https://github.com/reactphp/event-loop/issues/206
|
||||
$r = null;
|
||||
$w = array($this->resource);
|
||||
$e = DIRECTORY_SEPARATOR === '\\' ? $w : null;
|
||||
$ret = @socket_select($r, $w, $e, $timeout === null ? null : (int) $timeout, (int) (($timeout - floor($timeout)) * 1000000));
|
||||
|
||||
if ($ret === false) {
|
||||
throw Exception::createFromGlobalSocketOperation('Failed to select socket for writing');
|
||||
} elseif ($ret === 0) {
|
||||
throw new Exception('Timed out while waiting for connection', SOCKET_ETIMEDOUT);
|
||||
}
|
||||
|
||||
// confirm connection success (or fail if connected has been rejected)
|
||||
$this->assertAlive();
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* get socket option
|
||||
*
|
||||
* @param int $level
|
||||
* @param int $optname
|
||||
* @return mixed
|
||||
* @throws Exception on error
|
||||
* @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
|
||||
* @uses socket_get_option()
|
||||
*/
|
||||
public function getOption($level, $optname)
|
||||
{
|
||||
$value = @socket_get_option($this->resource, $level, $optname);
|
||||
if ($value === false) {
|
||||
throw Exception::createFromSocketResource($this->resource);
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* get remote side's address/path
|
||||
*
|
||||
* @return string
|
||||
* @throws Exception on error
|
||||
* @throws \Error PHP 8 only: throws \Error when socket is invalid
|
||||
* @uses socket_getpeername()
|
||||
*/
|
||||
public function getPeerName()
|
||||
{
|
||||
$ret = @socket_getpeername($this->resource, $address, $port);
|
||||
if ($ret === false) {
|
||||
throw Exception::createFromSocketResource($this->resource);
|
||||
}
|
||||
return $this->formatAddress($address, $port);
|
||||
}
|
||||
|
||||
/**
|
||||
* get local side's address/path
|
||||
*
|
||||
* @return string
|
||||
* @throws Exception on error
|
||||
* @throws \Error PHP 8 only: throws \Error when socket is invalid
|
||||
* @uses socket_getsockname()
|
||||
*/
|
||||
public function getSockName()
|
||||
{
|
||||
$ret = @socket_getsockname($this->resource, $address, $port);
|
||||
if ($ret === false) {
|
||||
throw Exception::createFromSocketResource($this->resource);
|
||||
}
|
||||
return $this->formatAddress($address, $port);
|
||||
}
|
||||
|
||||
/**
|
||||
* start listen for incoming connections
|
||||
*
|
||||
* @param int $backlog maximum number of incoming connections to be queued
|
||||
* @return self $this (chainable)
|
||||
* @throws Exception on error
|
||||
* @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
|
||||
* @see self::bind() has to be called first to bind name to socket
|
||||
* @uses socket_listen()
|
||||
*/
|
||||
public function listen($backlog = 0)
|
||||
{
|
||||
$ret = @socket_listen($this->resource, $backlog);
|
||||
if ($ret === false) {
|
||||
throw Exception::createFromSocketResource($this->resource);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* read up to $length bytes from connect()ed / accept()ed socket
|
||||
*
|
||||
* The $type parameter specifies if this should use either binary safe reading
|
||||
* (PHP_BINARY_READ, the default) or stop at CR or LF characters (PHP_NORMAL_READ)
|
||||
*
|
||||
* @param int $length maximum length to read
|
||||
* @param int $type either of PHP_BINARY_READ (the default) or PHP_NORMAL_READ
|
||||
* @return string
|
||||
* @throws Exception on error
|
||||
* @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
|
||||
* @see self::recv() if you need to pass flags
|
||||
* @uses socket_read()
|
||||
*/
|
||||
public function read($length, $type = PHP_BINARY_READ)
|
||||
{
|
||||
$data = @socket_read($this->resource, $length, $type);
|
||||
if ($data === false) {
|
||||
throw Exception::createFromSocketResource($this->resource);
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* receive up to $length bytes from connect()ed / accept()ed socket
|
||||
*
|
||||
* @param int $length maximum length to read
|
||||
* @param int $flags
|
||||
* @return string
|
||||
* @throws Exception on error
|
||||
* @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
|
||||
* @see self::read() if you do not need to pass $flags
|
||||
* @see self::recvFrom() if your socket is not connect()ed
|
||||
* @uses socket_recv()
|
||||
*/
|
||||
public function recv($length, $flags)
|
||||
{
|
||||
$ret = @socket_recv($this->resource, $buffer, $length, $flags);
|
||||
if ($ret === false) {
|
||||
throw Exception::createFromSocketResource($this->resource);
|
||||
}
|
||||
return $buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* receive up to $length bytes from socket
|
||||
*
|
||||
* @param int $length maximum length to read
|
||||
* @param int $flags
|
||||
* @param string $remote reference will be filled with remote/peer address/path
|
||||
* @return string
|
||||
* @throws Exception on error
|
||||
* @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
|
||||
* @see self::recv() if your socket is connect()ed
|
||||
* @uses socket_recvfrom()
|
||||
*/
|
||||
public function recvFrom($length, $flags, &$remote)
|
||||
{
|
||||
$ret = @socket_recvfrom($this->resource, $buffer, $length, $flags, $address, $port);
|
||||
if ($ret === false) {
|
||||
throw Exception::createFromSocketResource($this->resource);
|
||||
}
|
||||
$remote = $this->formatAddress($address, $port);
|
||||
return $buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* check socket to see if a read/recv/revFrom will not block
|
||||
*
|
||||
* @param float|null $sec maximum time to wait (in seconds), 0 = immediate polling, null = no limit
|
||||
* @return boolean true = socket ready (read will not block), false = timeout expired, socket is not ready
|
||||
* @throws Exception on error
|
||||
* @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
|
||||
* @uses socket_select()
|
||||
*/
|
||||
public function selectRead($sec = 0)
|
||||
{
|
||||
$usec = $sec === null ? 0 : (int) (($sec - floor($sec)) * 1000000);
|
||||
$r = array($this->resource);
|
||||
$n = null;
|
||||
$ret = @socket_select($r, $n, $n, $sec === null ? null : (int) $sec, $usec);
|
||||
if ($ret === false) {
|
||||
throw Exception::createFromGlobalSocketOperation('Failed to select socket for reading');
|
||||
}
|
||||
return !!$ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* check socket to see if a write/send/sendTo will not block
|
||||
*
|
||||
* @param float|null $sec maximum time to wait (in seconds), 0 = immediate polling, null = no limit
|
||||
* @return boolean true = socket ready (write will not block), false = timeout expired, socket is not ready
|
||||
* @throws Exception on error
|
||||
* @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
|
||||
* @uses socket_select()
|
||||
*/
|
||||
public function selectWrite($sec = 0)
|
||||
{
|
||||
$usec = $sec === null ? 0 : (int) (($sec - floor($sec)) * 1000000);
|
||||
$w = array($this->resource);
|
||||
$n = null;
|
||||
$ret = @socket_select($n, $w, $n, $sec === null ? null : (int) $sec, $usec);
|
||||
if ($ret === false) {
|
||||
throw Exception::createFromGlobalSocketOperation('Failed to select socket for writing');
|
||||
}
|
||||
return !!$ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* send given $buffer to connect()ed / accept()ed socket
|
||||
*
|
||||
* @param string $buffer
|
||||
* @param int $flags
|
||||
* @return int number of bytes actually written (make sure to check against given buffer length!)
|
||||
* @throws Exception on error
|
||||
* @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
|
||||
* @see self::write() if you do not need to pass $flags
|
||||
* @see self::sendTo() if your socket is not connect()ed
|
||||
* @uses socket_send()
|
||||
*/
|
||||
public function send($buffer, $flags)
|
||||
{
|
||||
$ret = @socket_send($this->resource, $buffer, strlen($buffer), $flags);
|
||||
if ($ret === false) {
|
||||
throw Exception::createFromSocketResource($this->resource);
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* send given $buffer to socket
|
||||
*
|
||||
* @param string $buffer
|
||||
* @param int $flags
|
||||
* @param string $remote remote/peer address/path
|
||||
* @return int number of bytes actually written
|
||||
* @throws Exception on error
|
||||
* @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
|
||||
* @see self::send() if your socket is connect()ed
|
||||
* @uses socket_sendto()
|
||||
*/
|
||||
public function sendTo($buffer, $flags, $remote)
|
||||
{
|
||||
$ret = @socket_sendto($this->resource, $buffer, strlen($buffer), $flags, $this->unformatAddress($remote, $port), $port);
|
||||
if ($ret === false) {
|
||||
throw Exception::createFromSocketResource($this->resource);
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* enable/disable blocking/nonblocking mode (O_NONBLOCK flag)
|
||||
*
|
||||
* @param boolean $toggle
|
||||
* @return self $this (chainable)
|
||||
* @throws Exception on error
|
||||
* @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
|
||||
* @uses socket_set_block()
|
||||
* @uses socket_set_nonblock()
|
||||
*/
|
||||
public function setBlocking($toggle = true)
|
||||
{
|
||||
$ret = $toggle ? @socket_set_block($this->resource) : @socket_set_nonblock($this->resource);
|
||||
if ($ret === false) {
|
||||
throw Exception::createFromSocketResource($this->resource);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* set socket option
|
||||
*
|
||||
* @param int $level
|
||||
* @param int $optname
|
||||
* @param mixed $optval
|
||||
* @return self $this (chainable)
|
||||
* @throws Exception on error
|
||||
* @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
|
||||
* @see self::getOption()
|
||||
* @uses socket_set_option()
|
||||
*/
|
||||
public function setOption($level, $optname, $optval)
|
||||
{
|
||||
$ret = @socket_set_option($this->resource, $level, $optname, $optval);
|
||||
if ($ret === false) {
|
||||
throw Exception::createFromSocketResource($this->resource);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* shuts down socket for receiving, sending or both
|
||||
*
|
||||
* @param int $how 0 = shutdown reading, 1 = shutdown writing, 2 = shutdown reading and writing
|
||||
* @return self $this (chainable)
|
||||
* @throws Exception on error
|
||||
* @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
|
||||
* @see self::close()
|
||||
* @uses socket_shutdown()
|
||||
*/
|
||||
public function shutdown($how = 2)
|
||||
{
|
||||
$ret = @socket_shutdown($this->resource, $how);
|
||||
if ($ret === false) {
|
||||
throw Exception::createFromSocketResource($this->resource);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* write $buffer to connect()ed / accept()ed socket
|
||||
*
|
||||
* @param string $buffer
|
||||
* @return int number of bytes actually written
|
||||
* @throws Exception on error
|
||||
* @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
|
||||
* @see self::send() if you need to pass flags
|
||||
* @uses socket_write()
|
||||
*/
|
||||
public function write($buffer)
|
||||
{
|
||||
$ret = @socket_write($this->resource, $buffer);
|
||||
if ($ret === false) {
|
||||
throw Exception::createFromSocketResource($this->resource);
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* get socket type as passed to socket_create()
|
||||
*
|
||||
* @return int usually either SOCK_STREAM or SOCK_DGRAM
|
||||
* @throws Exception on error
|
||||
* @throws \Error PHP 8 only: throws \Error when socket is invalid
|
||||
* @uses self::getOption()
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
return $this->getOption(SOL_SOCKET, SO_TYPE);
|
||||
}
|
||||
|
||||
/**
|
||||
* assert that this socket is alive and its error code is 0
|
||||
*
|
||||
* This will fetch and reset the current socket error code from the
|
||||
* socket and options and will throw an Exception along with error
|
||||
* message and code if the code is not 0, i.e. if it does indicate
|
||||
* an error situation.
|
||||
*
|
||||
* Calling this method should not be needed in most cases and is
|
||||
* likely to not throw an Exception. Each socket operation like
|
||||
* connect(), send(), etc. will throw a dedicated Exception in case
|
||||
* of an error anyway.
|
||||
*
|
||||
* @return self $this (chainable)
|
||||
* @throws Exception if error code is not 0
|
||||
* @throws \Error PHP 8 only: throws \Error when socket is invalid
|
||||
* @uses self::getOption() to retrieve and clear current error code
|
||||
* @uses self::getErrorMessage() to translate error code to
|
||||
*/
|
||||
public function assertAlive()
|
||||
{
|
||||
$code = $this->getOption(SOL_SOCKET, SO_ERROR);
|
||||
if ($code !== 0) {
|
||||
throw Exception::createFromCode($code, 'Socket error');
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* format given address/host/path and port
|
||||
*
|
||||
* @param string $address
|
||||
* @param int $port
|
||||
* @return string
|
||||
*/
|
||||
protected function formatAddress($address, $port)
|
||||
{
|
||||
if ($port !== 0) {
|
||||
if (strpos($address, ':') !== false) {
|
||||
$address = '[' . $address . ']';
|
||||
}
|
||||
$address .= ':' . $port;
|
||||
}
|
||||
return $address;
|
||||
}
|
||||
|
||||
/**
|
||||
* format given address by splitting it into returned address and port set by reference
|
||||
*
|
||||
* @param string $address
|
||||
* @param int $port
|
||||
* @return string address with port removed
|
||||
*/
|
||||
protected function unformatAddress($address, &$port)
|
||||
{
|
||||
// [::1]:2 => ::1 2
|
||||
// test:2 => test 2
|
||||
// ::1 => ::1
|
||||
// test => test
|
||||
|
||||
$colon = strrpos($address, ':');
|
||||
|
||||
// there is a colon and this is the only colon or there's a closing IPv6 bracket right before it
|
||||
if ($colon !== false && (strpos($address, ':') === $colon || strpos($address, ']') === ($colon - 1))) {
|
||||
$port = (int)substr($address, $colon + 1);
|
||||
$address = substr($address, 0, $colon);
|
||||
|
||||
// remove IPv6 square brackets
|
||||
if (substr($address, 0, 1) === '[') {
|
||||
$address = substr($address, 1, -1);
|
||||
}
|
||||
}
|
||||
return $address;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2011 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.
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"name": "clue/socks-react",
|
||||
"description": "Async SOCKS proxy connector client and server implementation, tunnel any TCP/IP-based protocol through a SOCKS5 or SOCKS4(a) proxy server, built on top of ReactPHP.",
|
||||
"keywords": ["socks client", "socks server", "socks5", "socks4a", "proxy server", "tcp tunnel", "async", "ReactPHP"],
|
||||
"homepage": "https://github.com/clue/reactphp-socks",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Christian Lück",
|
||||
"email": "christian@clue.engineering"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=5.3",
|
||||
"react/promise": "^3 || ^2.1 || ^1.2",
|
||||
"react/socket": "^1.12"
|
||||
},
|
||||
"require-dev": {
|
||||
"clue/block-react": "^1.5",
|
||||
"clue/connection-manager-extra": "^1.3",
|
||||
"phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35",
|
||||
"react/event-loop": "^1.2",
|
||||
"react/http": "^1.6"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": { "Clue\\React\\Socks\\": "src/" }
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": { "Clue\\Tests\\React\\Socks\\": "tests/" }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,458 @@
|
|||
<?php
|
||||
|
||||
namespace Clue\React\Socks;
|
||||
|
||||
use React\Promise;
|
||||
use React\Promise\PromiseInterface;
|
||||
use React\Promise\Deferred;
|
||||
use React\Socket\ConnectionInterface;
|
||||
use React\Socket\Connector;
|
||||
use React\Socket\ConnectorInterface;
|
||||
use React\Socket\FixedUriConnector;
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
|
||||
final class Client implements ConnectorInterface
|
||||
{
|
||||
/**
|
||||
* @var ConnectorInterface
|
||||
*/
|
||||
private $connector;
|
||||
|
||||
private $socksUri;
|
||||
|
||||
private $protocolVersion = 5;
|
||||
|
||||
private $auth = null;
|
||||
|
||||
/**
|
||||
* @param string $socksUri
|
||||
* @param ?ConnectorInterface $connector
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function __construct(
|
||||
#[\SensitiveParameter]
|
||||
$socksUri,
|
||||
ConnectorInterface $connector = null
|
||||
) {
|
||||
// support `sockss://` scheme for SOCKS over TLS
|
||||
// support `socks+unix://` scheme for Unix domain socket (UDS) paths
|
||||
if (preg_match('/^(socks(?:5|4)?)(s|\+unix):\/\/(.*?@)?(.+?)$/', $socksUri, $match)) {
|
||||
// rewrite URI to parse SOCKS scheme, authentication and dummy host
|
||||
$socksUri = $match[1] . '://' . $match[3] . 'localhost';
|
||||
|
||||
// connector uses appropriate transport scheme and explicit host given
|
||||
$connector = new FixedUriConnector(
|
||||
($match[2] === 's' ? 'tls://' : 'unix://') . $match[4],
|
||||
$connector ?: new Connector()
|
||||
);
|
||||
}
|
||||
|
||||
// assume default scheme if none is given
|
||||
if (strpos($socksUri, '://') === false) {
|
||||
$socksUri = 'socks://' . $socksUri;
|
||||
}
|
||||
|
||||
// parse URI into individual parts
|
||||
$parts = parse_url($socksUri);
|
||||
if (!$parts || !isset($parts['scheme'], $parts['host'])) {
|
||||
throw new InvalidArgumentException('Invalid SOCKS server URI "' . $socksUri . '"');
|
||||
}
|
||||
|
||||
// assume default port
|
||||
if (!isset($parts['port'])) {
|
||||
$parts['port'] = 1080;
|
||||
}
|
||||
|
||||
// user or password in URI => SOCKS5 authentication
|
||||
if (isset($parts['user']) || isset($parts['pass'])) {
|
||||
if ($parts['scheme'] !== 'socks' && $parts['scheme'] !== 'socks5') {
|
||||
// fail if any other protocol version given explicitly
|
||||
throw new InvalidArgumentException('Authentication requires SOCKS5. Consider using protocol version 5 or waive authentication');
|
||||
}
|
||||
$parts += array('user' => '', 'pass' => '');
|
||||
$this->setAuth(rawurldecode($parts['user']), rawurldecode($parts['pass']));
|
||||
}
|
||||
|
||||
// check for valid protocol version from URI scheme
|
||||
$this->setProtocolVersionFromScheme($parts['scheme']);
|
||||
|
||||
$this->socksUri = $parts['host'] . ':' . $parts['port'];
|
||||
$this->connector = $connector ?: new Connector();
|
||||
}
|
||||
|
||||
private function setProtocolVersionFromScheme($scheme)
|
||||
{
|
||||
if ($scheme === 'socks' || $scheme === 'socks5') {
|
||||
$this->protocolVersion = 5;
|
||||
} elseif ($scheme === 'socks4') {
|
||||
$this->protocolVersion = 4;
|
||||
} else {
|
||||
throw new InvalidArgumentException('Invalid protocol version given "' . $scheme . '://"');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* set login data for username/password authentication method (RFC1929)
|
||||
*
|
||||
* @param string $username
|
||||
* @param string $password
|
||||
* @link http://tools.ietf.org/html/rfc1929
|
||||
*/
|
||||
private function setAuth(
|
||||
$username,
|
||||
#[\SensitiveParameter]
|
||||
$password
|
||||
) {
|
||||
if (strlen($username) > 255 || strlen($password) > 255) {
|
||||
throw new InvalidArgumentException('Both username and password MUST NOT exceed a length of 255 bytes each');
|
||||
}
|
||||
$this->auth = pack('C2', 0x01, strlen($username)) . $username . pack('C', strlen($password)) . $password;
|
||||
}
|
||||
|
||||
/**
|
||||
* Establish a TCP/IP connection to the given target URI through the SOCKS server
|
||||
*
|
||||
* Many higher-level networking protocols build on top of TCP. It you're dealing
|
||||
* with one such client implementation, it probably uses/accepts an instance
|
||||
* implementing ReactPHP's `ConnectorInterface` (and usually its default `Connector`
|
||||
* instance). In this case you can also pass this `Connector` instance instead
|
||||
* to make this client implementation SOCKS-aware. That's it.
|
||||
*
|
||||
* @param string $uri
|
||||
* @return PromiseInterface Promise<ConnectionInterface,Exception>
|
||||
*/
|
||||
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('Invalid target URI specified'));
|
||||
}
|
||||
|
||||
$host = trim($parts['host'], '[]');
|
||||
$port = $parts['port'];
|
||||
|
||||
if (strlen($host) > 255 || $port > 65535 || $port < 0 || (string)$port !== (string)(int)$port) {
|
||||
return Promise\reject(new InvalidArgumentException('Invalid target specified'));
|
||||
}
|
||||
|
||||
// construct URI to SOCKS server to connect to
|
||||
$socksUri = $this->socksUri;
|
||||
|
||||
// append path from URI if given
|
||||
if (isset($parts['path'])) {
|
||||
$socksUri .= $parts['path'];
|
||||
}
|
||||
|
||||
// parse query args
|
||||
$args = array();
|
||||
if (isset($parts['query'])) {
|
||||
parse_str($parts['query'], $args);
|
||||
}
|
||||
|
||||
// append hostname from URI to query string unless explicitly given
|
||||
if (!isset($args['hostname'])) {
|
||||
$args['hostname'] = $host;
|
||||
}
|
||||
|
||||
// append query string
|
||||
$socksUri .= '?' . http_build_query($args, '', '&');
|
||||
|
||||
// append fragment from URI if given
|
||||
if (isset($parts['fragment'])) {
|
||||
$socksUri .= '#' . $parts['fragment'];
|
||||
}
|
||||
|
||||
// start TCP/IP connection to SOCKS server
|
||||
$connecting = $this->connector->connect($socksUri);
|
||||
|
||||
$deferred = new Deferred(function ($_, $reject) use ($uri, $connecting) {
|
||||
$reject(new RuntimeException(
|
||||
'Connection to ' . $uri . ' cancelled while waiting for proxy (ECONNABORTED)',
|
||||
defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103
|
||||
));
|
||||
|
||||
// either close active connection or cancel pending connection attempt
|
||||
$connecting->then(function (ConnectionInterface $stream) {
|
||||
$stream->close();
|
||||
});
|
||||
$connecting->cancel();
|
||||
});
|
||||
|
||||
// handle SOCKS protocol once connection is ready
|
||||
// resolve plain connection once SOCKS protocol is completed
|
||||
$that = $this;
|
||||
$connecting->then(
|
||||
function (ConnectionInterface $stream) use ($that, $host, $port, $deferred, $uri) {
|
||||
$that->handleConnectedSocks($stream, $host, $port, $deferred, $uri);
|
||||
},
|
||||
function (Exception $e) use ($uri, $deferred) {
|
||||
$deferred->reject($e = new RuntimeException(
|
||||
'Connection to ' . $uri . ' failed because connection to proxy failed (ECONNREFUSED)',
|
||||
defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111,
|
||||
$e
|
||||
));
|
||||
|
||||
// avoid garbage references by replacing all closures in call stack.
|
||||
// what a lovely piece of code!
|
||||
$r = new \ReflectionProperty('Exception', 'trace');
|
||||
$r->setAccessible(true);
|
||||
$trace = $r->getValue($e);
|
||||
|
||||
// Exception trace arguments are not available on some PHP 7.4 installs
|
||||
// @codeCoverageIgnoreStart
|
||||
foreach ($trace as $ti => $one) {
|
||||
if (isset($one['args'])) {
|
||||
foreach ($one['args'] as $ai => $arg) {
|
||||
if ($arg instanceof \Closure) {
|
||||
$trace[$ti]['args'][$ai] = 'Object(' . \get_class($arg) . ')';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
$r->setValue($e, $trace);
|
||||
}
|
||||
);
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal helper used to handle the communication with the SOCKS server
|
||||
*
|
||||
* @param ConnectionInterface $stream
|
||||
* @param string $host
|
||||
* @param int $port
|
||||
* @param Deferred $deferred
|
||||
* @param string $uri
|
||||
* @return void
|
||||
* @internal
|
||||
*/
|
||||
public function handleConnectedSocks(ConnectionInterface $stream, $host, $port, Deferred $deferred, $uri)
|
||||
{
|
||||
$reader = new StreamReader();
|
||||
$stream->on('data', array($reader, 'write'));
|
||||
|
||||
$stream->on('error', $onError = function (Exception $e) use ($deferred, $uri) {
|
||||
$deferred->reject(new RuntimeException(
|
||||
'Connection to ' . $uri . ' failed because connection to proxy caused a stream error (EIO)',
|
||||
defined('SOCKET_EIO') ? SOCKET_EIO : 5, $e)
|
||||
);
|
||||
});
|
||||
|
||||
$stream->on('close', $onClose = function () use ($deferred, $uri) {
|
||||
$deferred->reject(new RuntimeException(
|
||||
'Connection to ' . $uri . ' failed because connection to proxy was lost while waiting for response from proxy (ECONNRESET)',
|
||||
defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 104)
|
||||
);
|
||||
});
|
||||
|
||||
if ($this->protocolVersion === 5) {
|
||||
$promise = $this->handleSocks5($stream, $host, $port, $reader, $uri);
|
||||
} else {
|
||||
$promise = $this->handleSocks4($stream, $host, $port, $reader, $uri);
|
||||
}
|
||||
|
||||
$promise->then(function () use ($deferred, $stream, $reader, $onError, $onClose) {
|
||||
$stream->removeListener('data', array($reader, 'write'));
|
||||
$stream->removeListener('error', $onError);
|
||||
$stream->removeListener('close', $onClose);
|
||||
|
||||
$deferred->resolve($stream);
|
||||
}, function (Exception $error) use ($deferred, $stream, $uri) {
|
||||
// pass custom RuntimeException through as-is, otherwise wrap in protocol error
|
||||
if (!$error instanceof RuntimeException) {
|
||||
$error = new RuntimeException(
|
||||
'Connection to ' . $uri . ' failed because proxy returned invalid response (EBADMSG)',
|
||||
defined('SOCKET_EBADMSG') ? SOCKET_EBADMSG: 71,
|
||||
$error
|
||||
);
|
||||
}
|
||||
|
||||
$deferred->reject($error);
|
||||
$stream->close();
|
||||
});
|
||||
}
|
||||
|
||||
private function handleSocks4(ConnectionInterface $stream, $host, $port, StreamReader $reader, $uri)
|
||||
{
|
||||
// do not resolve hostname. only try to convert to IP
|
||||
$ip = ip2long($host);
|
||||
|
||||
// send IP or (0.0.0.1) if invalid
|
||||
$data = pack('C2nNC', 0x04, 0x01, $port, $ip === false ? 1 : $ip, 0x00);
|
||||
|
||||
if ($ip === false) {
|
||||
// host is not a valid IP => send along hostname (SOCKS4a)
|
||||
$data .= $host . pack('C', 0x00);
|
||||
}
|
||||
|
||||
$stream->write($data);
|
||||
|
||||
return $reader->readBinary(array(
|
||||
'null' => 'C',
|
||||
'status' => 'C',
|
||||
'port' => 'n',
|
||||
'ip' => 'N'
|
||||
))->then(function ($data) use ($uri) {
|
||||
if ($data['null'] !== 0x00) {
|
||||
throw new Exception('Invalid SOCKS response');
|
||||
}
|
||||
if ($data['status'] !== 0x5a) {
|
||||
throw new RuntimeException(
|
||||
'Connection to ' . $uri . ' failed because proxy refused connection with error code ' . sprintf('0x%02X', $data['status']) . ' (ECONNREFUSED)',
|
||||
defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private function handleSocks5(ConnectionInterface $stream, $host, $port, StreamReader $reader, $uri)
|
||||
{
|
||||
// protocol version 5
|
||||
$data = pack('C', 0x05);
|
||||
|
||||
$auth = $this->auth;
|
||||
if ($auth === null) {
|
||||
// one method, no authentication
|
||||
$data .= pack('C2', 0x01, 0x00);
|
||||
} else {
|
||||
// two methods, username/password and no authentication
|
||||
$data .= pack('C3', 0x02, 0x02, 0x00);
|
||||
}
|
||||
$stream->write($data);
|
||||
|
||||
$that = $this;
|
||||
|
||||
return $reader->readBinary(array(
|
||||
'version' => 'C',
|
||||
'method' => 'C'
|
||||
))->then(function ($data) use ($auth, $stream, $reader, $uri) {
|
||||
if ($data['version'] !== 0x05) {
|
||||
throw new Exception('Version/Protocol mismatch');
|
||||
}
|
||||
|
||||
if ($data['method'] === 0x02 && $auth !== null) {
|
||||
// username/password authentication requested and provided
|
||||
$stream->write($auth);
|
||||
|
||||
return $reader->readBinary(array(
|
||||
'version' => 'C',
|
||||
'status' => 'C'
|
||||
))->then(function ($data) use ($uri) {
|
||||
if ($data['version'] !== 0x01 || $data['status'] !== 0x00) {
|
||||
throw new RuntimeException(
|
||||
'Connection to ' . $uri . ' failed because proxy denied access with given authentication details (EACCES)',
|
||||
defined('SOCKET_EACCES') ? SOCKET_EACCES : 13
|
||||
);
|
||||
}
|
||||
});
|
||||
} else if ($data['method'] !== 0x00) {
|
||||
// any other method than "no authentication"
|
||||
throw new RuntimeException(
|
||||
'Connection to ' . $uri . ' failed because proxy denied access due to unsupported authentication method (EACCES)',
|
||||
defined('SOCKET_EACCES') ? SOCKET_EACCES : 13
|
||||
);
|
||||
}
|
||||
})->then(function () use ($stream, $reader, $host, $port) {
|
||||
// do not resolve hostname. only try to convert to (binary/packed) IP
|
||||
$ip = @inet_pton($host);
|
||||
|
||||
$data = pack('C3', 0x05, 0x01, 0x00);
|
||||
if ($ip === false) {
|
||||
// not an IP, send as hostname
|
||||
$data .= pack('C2', 0x03, strlen($host)) . $host;
|
||||
} else {
|
||||
// send as IPv4 / IPv6
|
||||
$data .= pack('C', (strpos($host, ':') === false) ? 0x01 : 0x04) . $ip;
|
||||
}
|
||||
$data .= pack('n', $port);
|
||||
|
||||
$stream->write($data);
|
||||
|
||||
return $reader->readBinary(array(
|
||||
'version' => 'C',
|
||||
'status' => 'C',
|
||||
'null' => 'C',
|
||||
'type' => 'C'
|
||||
));
|
||||
})->then(function ($data) use ($reader, $uri) {
|
||||
if ($data['version'] !== 0x05 || $data['null'] !== 0x00) {
|
||||
throw new Exception('Invalid SOCKS response');
|
||||
}
|
||||
if ($data['status'] !== 0x00) {
|
||||
// map limited list of SOCKS error codes to common socket error conditions
|
||||
// @link https://tools.ietf.org/html/rfc1928#section-6
|
||||
if ($data['status'] === Server::ERROR_GENERAL) {
|
||||
throw new RuntimeException(
|
||||
'Connection to ' . $uri . ' failed because proxy refused connection with general server failure (ECONNREFUSED)',
|
||||
defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111
|
||||
);
|
||||
} elseif ($data['status'] === Server::ERROR_NOT_ALLOWED_BY_RULESET) {
|
||||
throw new RuntimeException(
|
||||
'Connection to ' . $uri . ' failed because proxy denied access due to ruleset (EACCES)',
|
||||
defined('SOCKET_EACCES') ? SOCKET_EACCES : 13
|
||||
);
|
||||
} elseif ($data['status'] === Server::ERROR_NETWORK_UNREACHABLE) {
|
||||
throw new RuntimeException(
|
||||
'Connection to ' . $uri . ' failed because proxy reported network unreachable (ENETUNREACH)',
|
||||
defined('SOCKET_ENETUNREACH') ? SOCKET_ENETUNREACH : 101
|
||||
);
|
||||
} elseif ($data['status'] === Server::ERROR_HOST_UNREACHABLE) {
|
||||
throw new RuntimeException(
|
||||
'Connection to ' . $uri . ' failed because proxy reported host unreachable (EHOSTUNREACH)',
|
||||
defined('SOCKET_EHOSTUNREACH') ? SOCKET_EHOSTUNREACH : 113
|
||||
);
|
||||
} elseif ($data['status'] === Server::ERROR_CONNECTION_REFUSED) {
|
||||
throw new RuntimeException(
|
||||
'Connection to ' . $uri . ' failed because proxy reported connection refused (ECONNREFUSED)',
|
||||
defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111
|
||||
);
|
||||
} elseif ($data['status'] === Server::ERROR_TTL) {
|
||||
throw new RuntimeException(
|
||||
'Connection to ' . $uri . ' failed because proxy reported TTL/timeout expired (ETIMEDOUT)',
|
||||
defined('SOCKET_ETIMEDOUT') ? SOCKET_ETIMEDOUT : 110
|
||||
);
|
||||
} elseif ($data['status'] === Server::ERROR_COMMAND_UNSUPPORTED) {
|
||||
throw new RuntimeException(
|
||||
'Connection to ' . $uri . ' failed because proxy does not support the CONNECT command (EPROTO)',
|
||||
defined('SOCKET_EPROTO') ? SOCKET_EPROTO : 71
|
||||
);
|
||||
} elseif ($data['status'] === Server::ERROR_ADDRESS_UNSUPPORTED) {
|
||||
throw new RuntimeException(
|
||||
'Connection to ' . $uri . ' failed because proxy does not support this address type (EPROTO)',
|
||||
defined('SOCKET_EPROTO') ? SOCKET_EPROTO : 71
|
||||
);
|
||||
}
|
||||
|
||||
throw new RuntimeException(
|
||||
'Connection to ' . $uri . ' failed because proxy server refused connection with unknown error code ' . sprintf('0x%02X', $data['status']) . ' (ECONNREFUSED)',
|
||||
defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111
|
||||
);
|
||||
}
|
||||
if ($data['type'] === 0x01) {
|
||||
// IPv4 address => skip IP and port
|
||||
return $reader->readLength(6);
|
||||
} elseif ($data['type'] === 0x03) {
|
||||
// domain name => read domain name length
|
||||
return $reader->readBinary(array(
|
||||
'length' => 'C'
|
||||
))->then(function ($data) use ($reader) {
|
||||
// skip domain name and port
|
||||
return $reader->readLength($data['length'] + 2);
|
||||
});
|
||||
} elseif ($data['type'] === 0x04) {
|
||||
// IPv6 address => skip IP and port
|
||||
return $reader->readLength(18);
|
||||
} else {
|
||||
throw new Exception('Invalid SOCKS reponse: Invalid address type');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,416 @@
|
|||
<?php
|
||||
|
||||
namespace Clue\React\Socks;
|
||||
|
||||
use React\Socket\ServerInterface;
|
||||
use React\Promise\PromiseInterface;
|
||||
use React\Socket\ConnectorInterface;
|
||||
use React\Socket\Connector;
|
||||
use React\Socket\ConnectionInterface;
|
||||
use React\EventLoop\Loop;
|
||||
use React\EventLoop\LoopInterface;
|
||||
use \UnexpectedValueException;
|
||||
use \InvalidArgumentException;
|
||||
use \Exception;
|
||||
use React\Promise\Timer\TimeoutException;
|
||||
|
||||
final class Server
|
||||
{
|
||||
// the following error codes are only used for SOCKS5 only
|
||||
/** @internal */
|
||||
const ERROR_GENERAL = 0x01;
|
||||
/** @internal */
|
||||
const ERROR_NOT_ALLOWED_BY_RULESET = 0x02;
|
||||
/** @internal */
|
||||
const ERROR_NETWORK_UNREACHABLE = 0x03;
|
||||
/** @internal */
|
||||
const ERROR_HOST_UNREACHABLE = 0x04;
|
||||
/** @internal */
|
||||
const ERROR_CONNECTION_REFUSED = 0x05;
|
||||
/** @internal */
|
||||
const ERROR_TTL = 0x06;
|
||||
/** @internal */
|
||||
const ERROR_COMMAND_UNSUPPORTED = 0x07;
|
||||
/** @internal */
|
||||
const ERROR_ADDRESS_UNSUPPORTED = 0x08;
|
||||
|
||||
/** @var LoopInterface */
|
||||
private $loop;
|
||||
|
||||
/** @var ConnectorInterface */
|
||||
private $connector;
|
||||
|
||||
/**
|
||||
* @var null|callable
|
||||
*/
|
||||
private $auth;
|
||||
|
||||
/**
|
||||
*
|
||||
* This class takes an optional `LoopInterface|null $loop` parameter that can be used to
|
||||
* pass the event loop instance to use for this object. You can use a `null` value
|
||||
* here in order to use the [default loop](https://github.com/reactphp/event-loop#loop).
|
||||
* This value SHOULD NOT be given unless you're sure you want to explicitly use a
|
||||
* given event loop instance.
|
||||
*
|
||||
* @param ?LoopInterface $loop
|
||||
* @param ?ConnectorInterface $connector
|
||||
* @param null|array|callable $auth
|
||||
*/
|
||||
public function __construct(
|
||||
LoopInterface $loop = null,
|
||||
ConnectorInterface $connector = null,
|
||||
#[\SensitiveParameter]
|
||||
$auth = null
|
||||
) {
|
||||
if (\is_array($auth)) {
|
||||
// wrap authentication array in authentication callback
|
||||
$this->auth = function (
|
||||
$username,
|
||||
#[\SensitiveParameter]
|
||||
$password
|
||||
) use ($auth) {
|
||||
return \React\Promise\resolve(
|
||||
isset($auth[$username]) && (string)$auth[$username] === $password
|
||||
);
|
||||
};
|
||||
} elseif (\is_callable($auth)) {
|
||||
// wrap authentication callback in order to cast its return value to a promise
|
||||
$this->auth = function(
|
||||
$username,
|
||||
#[\SensitiveParameter]
|
||||
$password,
|
||||
#[\SensitiveParameter]
|
||||
$remote
|
||||
) use ($auth) {
|
||||
return \React\Promise\resolve(
|
||||
\call_user_func($auth, $username, $password, $remote)
|
||||
);
|
||||
};
|
||||
} elseif ($auth !== null) {
|
||||
throw new \InvalidArgumentException('Invalid authenticator given');
|
||||
}
|
||||
|
||||
$this->loop = $loop ?: Loop::get();
|
||||
$this->connector = $connector ?: new Connector(array(), $this->loop);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ServerInterface $socket
|
||||
* @return void
|
||||
*/
|
||||
public function listen(ServerInterface $socket)
|
||||
{
|
||||
$that = $this;
|
||||
$socket->on('connection', function ($connection) use ($that) {
|
||||
$that->onConnection($connection);
|
||||
});
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
public function onConnection(ConnectionInterface $connection)
|
||||
{
|
||||
$that = $this;
|
||||
$handling = $this->handleSocks($connection)->then(null, function () use ($connection, $that) {
|
||||
// SOCKS failed => close connection
|
||||
$that->endConnection($connection);
|
||||
});
|
||||
|
||||
$connection->on('close', function () use ($handling) {
|
||||
$handling->cancel();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* [internal] gracefully shutdown connection by flushing all remaining data and closing stream
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function endConnection(ConnectionInterface $stream)
|
||||
{
|
||||
$tid = true;
|
||||
$loop = $this->loop;
|
||||
|
||||
// cancel below timer in case connection is closed in time
|
||||
$stream->once('close', function () use (&$tid, $loop) {
|
||||
// close event called before the timer was set up, so everything is okay
|
||||
if ($tid === true) {
|
||||
// make sure to not start a useless timer
|
||||
$tid = false;
|
||||
} else {
|
||||
$loop->cancelTimer($tid);
|
||||
}
|
||||
});
|
||||
|
||||
// shut down connection by pausing input data, flushing outgoing buffer and then exit
|
||||
$stream->pause();
|
||||
$stream->end();
|
||||
|
||||
// check if connection is not already closed
|
||||
if ($tid === true) {
|
||||
// fall back to forcefully close connection in 3 seconds if buffer can not be flushed
|
||||
$tid = $loop->addTimer(3.0, array($stream,'close'));
|
||||
}
|
||||
}
|
||||
|
||||
private function handleSocks(ConnectionInterface $stream)
|
||||
{
|
||||
$reader = new StreamReader();
|
||||
$stream->on('data', array($reader, 'write'));
|
||||
|
||||
$that = $this;
|
||||
$auth = $this->auth;
|
||||
|
||||
return $reader->readByte()->then(function ($version) use ($stream, $that, $auth, $reader){
|
||||
if ($version === 0x04) {
|
||||
if ($auth !== null) {
|
||||
throw new UnexpectedValueException('SOCKS4 not allowed because authentication is required');
|
||||
}
|
||||
return $that->handleSocks4($stream, $reader);
|
||||
} else if ($version === 0x05) {
|
||||
return $that->handleSocks5($stream, $auth, $reader);
|
||||
}
|
||||
throw new UnexpectedValueException('Unexpected/unknown version number');
|
||||
});
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
public function handleSocks4(ConnectionInterface $stream, StreamReader $reader)
|
||||
{
|
||||
$remote = $stream->getRemoteAddress();
|
||||
if ($remote !== null) {
|
||||
// remove transport scheme and prefix socks4:// instead
|
||||
$secure = strpos($remote, 'tls://') === 0;
|
||||
if (($pos = strpos($remote, '://')) !== false) {
|
||||
$remote = substr($remote, $pos + 3);
|
||||
}
|
||||
$remote = 'socks4' . ($secure ? 's' : '') . '://' . $remote;
|
||||
}
|
||||
|
||||
$that = $this;
|
||||
return $reader->readByteAssert(0x01)->then(function () use ($reader) {
|
||||
return $reader->readBinary(array(
|
||||
'port' => 'n',
|
||||
'ipLong' => 'N',
|
||||
'null' => 'C'
|
||||
));
|
||||
})->then(function ($data) use ($reader, $remote) {
|
||||
if ($data['null'] !== 0x00) {
|
||||
throw new Exception('Not a null byte');
|
||||
}
|
||||
if ($data['ipLong'] === 0) {
|
||||
throw new Exception('Invalid IP');
|
||||
}
|
||||
if ($data['port'] === 0) {
|
||||
throw new Exception('Invalid port');
|
||||
}
|
||||
if ($data['ipLong'] < 256) {
|
||||
// invalid IP => probably a SOCKS4a request which appends the hostname
|
||||
return $reader->readStringNull()->then(function ($string) use ($data, $remote){
|
||||
return array($string, $data['port'], $remote);
|
||||
});
|
||||
} else {
|
||||
$ip = long2ip($data['ipLong']);
|
||||
return array($ip, $data['port'], $remote);
|
||||
}
|
||||
})->then(function ($target) use ($stream, $that) {
|
||||
return $that->connectTarget($stream, $target)->then(function (ConnectionInterface $remote) use ($stream){
|
||||
$stream->write(pack('C8', 0x00, 0x5a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00));
|
||||
|
||||
return $remote;
|
||||
}, function($error) use ($stream){
|
||||
$stream->end(pack('C8', 0x00, 0x5b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00));
|
||||
|
||||
throw $error;
|
||||
});
|
||||
}, function($error) {
|
||||
throw new UnexpectedValueException('SOCKS4 protocol error',0,$error);
|
||||
});
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
public function handleSocks5(ConnectionInterface $stream, $auth, StreamReader $reader)
|
||||
{
|
||||
$remote = $stream->getRemoteAddress();
|
||||
if ($remote !== null) {
|
||||
// remove transport scheme and prefix socks5:// instead
|
||||
$secure = strpos($remote, 'tls://') === 0;
|
||||
if (($pos = strpos($remote, '://')) !== false) {
|
||||
$remote = substr($remote, $pos + 3);
|
||||
}
|
||||
$remote = 'socks' . ($secure ? 's' : '') . '://' . $remote;
|
||||
}
|
||||
|
||||
$that = $this;
|
||||
return $reader->readByte()->then(function ($num) use ($reader) {
|
||||
// $num different authentication mechanisms offered
|
||||
return $reader->readLength($num);
|
||||
})->then(function ($methods) use ($reader, $stream, $auth, &$remote) {
|
||||
if ($auth === null && strpos($methods,"\x00") !== false) {
|
||||
// accept "no authentication"
|
||||
$stream->write(pack('C2', 0x05, 0x00));
|
||||
|
||||
return 0x00;
|
||||
} else if ($auth !== null && strpos($methods,"\x02") !== false) {
|
||||
// username/password authentication (RFC 1929) sub negotiation
|
||||
$stream->write(pack('C2', 0x05, 0x02));
|
||||
return $reader->readByteAssert(0x01)->then(function () use ($reader) {
|
||||
return $reader->readByte();
|
||||
})->then(function ($length) use ($reader) {
|
||||
return $reader->readLength($length);
|
||||
})->then(function ($username) use ($reader, $auth, $stream, &$remote) {
|
||||
return $reader->readByte()->then(function ($length) use ($reader) {
|
||||
return $reader->readLength($length);
|
||||
})->then(function (
|
||||
#[\SensitiveParameter]
|
||||
$password
|
||||
) use ($username, $auth, $stream, &$remote) {
|
||||
// username and password given => authenticate
|
||||
|
||||
// prefix username/password to remote URI
|
||||
if ($remote !== null) {
|
||||
$remote = str_replace('://', '://' . rawurlencode($username) . ':' . rawurlencode($password) . '@', $remote);
|
||||
}
|
||||
|
||||
return $auth($username, $password, $remote)->then(function ($authenticated) use ($stream) {
|
||||
if ($authenticated) {
|
||||
// accept auth
|
||||
$stream->write(pack('C2', 0x01, 0x00));
|
||||
} else {
|
||||
// reject auth => send any code but 0x00
|
||||
$stream->end(pack('C2', 0x01, 0xFF));
|
||||
throw new UnexpectedValueException('Authentication denied');
|
||||
}
|
||||
}, function ($e) use ($stream) {
|
||||
// reject failed authentication => send any code but 0x00
|
||||
$stream->end(pack('C2', 0x01, 0xFF));
|
||||
throw new UnexpectedValueException('Authentication error', 0, $e);
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// reject all offered authentication methods
|
||||
$stream->write(pack('C2', 0x05, 0xFF));
|
||||
throw new UnexpectedValueException('No acceptable authentication mechanism found');
|
||||
}
|
||||
})->then(function ($method) use ($reader) {
|
||||
return $reader->readBinary(array(
|
||||
'version' => 'C',
|
||||
'command' => 'C',
|
||||
'null' => 'C',
|
||||
'type' => 'C'
|
||||
));
|
||||
})->then(function ($data) use ($reader) {
|
||||
if ($data['version'] !== 0x05) {
|
||||
throw new UnexpectedValueException('Invalid SOCKS version');
|
||||
}
|
||||
if ($data['command'] !== 0x01) {
|
||||
throw new UnexpectedValueException('Only CONNECT requests supported', Server::ERROR_COMMAND_UNSUPPORTED);
|
||||
}
|
||||
// if ($data['null'] !== 0x00) {
|
||||
// throw new UnexpectedValueException('Reserved byte has to be NULL');
|
||||
// }
|
||||
if ($data['type'] === 0x03) {
|
||||
// target hostname string
|
||||
return $reader->readByte()->then(function ($len) use ($reader) {
|
||||
return $reader->readLength($len);
|
||||
});
|
||||
} else if ($data['type'] === 0x01) {
|
||||
// target IPv4
|
||||
return $reader->readLength(4)->then(function ($addr) {
|
||||
return inet_ntop($addr);
|
||||
});
|
||||
} else if ($data['type'] === 0x04) {
|
||||
// target IPv6
|
||||
return $reader->readLength(16)->then(function ($addr) {
|
||||
return inet_ntop($addr);
|
||||
});
|
||||
} else {
|
||||
throw new UnexpectedValueException('Invalid address type', Server::ERROR_ADDRESS_UNSUPPORTED);
|
||||
}
|
||||
})->then(function ($host) use ($reader, &$remote) {
|
||||
return $reader->readBinary(array('port'=>'n'))->then(function ($data) use ($host, &$remote) {
|
||||
return array($host, $data['port'], $remote);
|
||||
});
|
||||
})->then(function ($target) use ($that, $stream) {
|
||||
return $that->connectTarget($stream, $target);
|
||||
}, function($error) use ($stream) {
|
||||
throw new UnexpectedValueException('SOCKS5 protocol error', $error->getCode(), $error);
|
||||
})->then(function (ConnectionInterface $remote) use ($stream) {
|
||||
$stream->write(pack('C4Nn', 0x05, 0x00, 0x00, 0x01, 0, 0));
|
||||
|
||||
return $remote;
|
||||
}, function(Exception $error) use ($stream){
|
||||
$stream->write(pack('C4Nn', 0x05, $error->getCode() === 0 ? Server::ERROR_GENERAL : $error->getCode(), 0x00, 0x01, 0, 0));
|
||||
|
||||
throw $error;
|
||||
});
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
public function connectTarget(ConnectionInterface $stream, array $target)
|
||||
{
|
||||
$uri = $target[0];
|
||||
if (strpos($uri, ':') !== false) {
|
||||
$uri = '[' . $uri . ']';
|
||||
}
|
||||
$uri .= ':' . $target[1];
|
||||
|
||||
// validate URI so a string hostname can not pass excessive URI parts
|
||||
$parts = parse_url('tcp://' . $uri);
|
||||
if (!$parts || !isset($parts['scheme'], $parts['host'], $parts['port']) || count($parts) !== 3) {
|
||||
return \React\Promise\reject(new InvalidArgumentException('Invalid target URI given'));
|
||||
}
|
||||
|
||||
if (isset($target[2])) {
|
||||
$uri .= '?source=' . rawurlencode($target[2]);
|
||||
}
|
||||
|
||||
$that = $this;
|
||||
$connecting = $this->connector->connect($uri);
|
||||
|
||||
$stream->on('close', function () use ($connecting) {
|
||||
$connecting->cancel();
|
||||
});
|
||||
|
||||
return $connecting->then(function (ConnectionInterface $remote) use ($stream, $that) {
|
||||
$stream->pipe($remote, array('end'=>false));
|
||||
$remote->pipe($stream, array('end'=>false));
|
||||
|
||||
// remote end closes connection => stop reading from local end, try to flush buffer to local and disconnect local
|
||||
$remote->on('end', function() use ($stream, $that) {
|
||||
$that->endConnection($stream);
|
||||
});
|
||||
|
||||
// local end closes connection => stop reading from remote end, try to flush buffer to remote and disconnect remote
|
||||
$stream->on('end', function() use ($remote, $that) {
|
||||
$that->endConnection($remote);
|
||||
});
|
||||
|
||||
// set bigger buffer size of 100k to improve performance
|
||||
$stream->bufferSize = $remote->bufferSize = 100 * 1024 * 1024;
|
||||
|
||||
return $remote;
|
||||
}, function(Exception $error) {
|
||||
// default to general/unknown error
|
||||
$code = Server::ERROR_GENERAL;
|
||||
|
||||
// map common socket error conditions to limited list of SOCKS error codes
|
||||
if ((defined('SOCKET_EACCES') && $error->getCode() === SOCKET_EACCES) || $error->getCode() === 13) {
|
||||
$code = Server::ERROR_NOT_ALLOWED_BY_RULESET;
|
||||
} elseif ((defined('SOCKET_EHOSTUNREACH') && $error->getCode() === SOCKET_EHOSTUNREACH) || $error->getCode() === 113) {
|
||||
$code = Server::ERROR_HOST_UNREACHABLE;
|
||||
} elseif ((defined('SOCKET_ENETUNREACH') && $error->getCode() === SOCKET_ENETUNREACH) || $error->getCode() === 101) {
|
||||
$code = Server::ERROR_NETWORK_UNREACHABLE;
|
||||
} elseif ((defined('SOCKET_ECONNREFUSED') && $error->getCode() === SOCKET_ECONNREFUSED) || $error->getCode() === 111 || $error->getMessage() === 'Connection refused') {
|
||||
// Socket component does not currently assign an error code for this, so we have to resort to checking the exception message
|
||||
$code = Server::ERROR_CONNECTION_REFUSED;
|
||||
} elseif ((defined('SOCKET_ETIMEDOUT') && $error->getCode() === SOCKET_ETIMEDOUT) || $error->getCode() === 110 || $error instanceof TimeoutException) {
|
||||
// Socket component does not currently assign an error code for this, but we can rely on the TimeoutException
|
||||
$code = Server::ERROR_TTL;
|
||||
}
|
||||
|
||||
throw new UnexpectedValueException('Unable to connect to remote target', $code, $error);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,149 @@
|
|||
<?php
|
||||
|
||||
namespace Clue\React\Socks;
|
||||
|
||||
use React\Promise\Deferred;
|
||||
use \InvalidArgumentException;
|
||||
use \UnexpectedValueException;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class StreamReader
|
||||
{
|
||||
const RET_DONE = true;
|
||||
const RET_INCOMPLETE = null;
|
||||
|
||||
private $buffer = '';
|
||||
private $queue = array();
|
||||
|
||||
public function write($data)
|
||||
{
|
||||
$this->buffer .= $data;
|
||||
|
||||
do {
|
||||
$current = reset($this->queue);
|
||||
|
||||
if ($current === false) {
|
||||
break;
|
||||
}
|
||||
|
||||
/* @var $current Closure */
|
||||
|
||||
$ret = $current($this->buffer);
|
||||
|
||||
if ($ret === self::RET_INCOMPLETE) {
|
||||
// current is incomplete, so wait for further data to arrive
|
||||
break;
|
||||
} else {
|
||||
// current is done, remove from list and continue with next
|
||||
array_shift($this->queue);
|
||||
}
|
||||
} while (true);
|
||||
}
|
||||
|
||||
public function readBinary($structure)
|
||||
{
|
||||
$length = 0;
|
||||
$unpack = '';
|
||||
foreach ($structure as $name=>$format) {
|
||||
if ($length !== 0) {
|
||||
$unpack .= '/';
|
||||
}
|
||||
$unpack .= $format . $name;
|
||||
|
||||
if ($format === 'C') {
|
||||
++$length;
|
||||
} else if ($format === 'n') {
|
||||
$length += 2;
|
||||
} else if ($format === 'N') {
|
||||
$length += 4;
|
||||
} else {
|
||||
throw new InvalidArgumentException('Invalid format given');
|
||||
}
|
||||
}
|
||||
|
||||
return $this->readLength($length)->then(function ($response) use ($unpack) {
|
||||
return unpack($unpack, $response);
|
||||
});
|
||||
}
|
||||
|
||||
public function readLength($bytes)
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
|
||||
$this->readBufferCallback(function (&$buffer) use ($bytes, $deferred) {
|
||||
if (strlen($buffer) >= $bytes) {
|
||||
$deferred->resolve((string)substr($buffer, 0, $bytes));
|
||||
$buffer = (string)substr($buffer, $bytes);
|
||||
|
||||
return StreamReader::RET_DONE;
|
||||
}
|
||||
});
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
public function readByte()
|
||||
{
|
||||
return $this->readBinary(array(
|
||||
'byte' => 'C'
|
||||
))->then(function ($data) {
|
||||
return $data['byte'];
|
||||
});
|
||||
}
|
||||
|
||||
public function readByteAssert($expect)
|
||||
{
|
||||
return $this->readByte()->then(function ($byte) use ($expect) {
|
||||
if ($byte !== $expect) {
|
||||
throw new UnexpectedValueException('Unexpected byte encountered');
|
||||
}
|
||||
return $byte;
|
||||
});
|
||||
}
|
||||
|
||||
public function readStringNull()
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
$string = '';
|
||||
|
||||
$that = $this;
|
||||
$readOne = function () use (&$readOne, $that, $deferred, &$string) {
|
||||
$that->readByte()->then(function ($byte) use ($deferred, &$string, $readOne) {
|
||||
if ($byte === 0x00) {
|
||||
$deferred->resolve($string);
|
||||
} else {
|
||||
$string .= chr($byte);
|
||||
$readOne();
|
||||
}
|
||||
});
|
||||
};
|
||||
$readOne();
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
|
||||
public function readBufferCallback(/* callable */ $callable)
|
||||
{
|
||||
if (!is_callable($callable)) {
|
||||
throw new InvalidArgumentException('Given function must be callable');
|
||||
}
|
||||
|
||||
if ($this->queue) {
|
||||
$this->queue []= $callable;
|
||||
} else {
|
||||
$this->queue = array($callable);
|
||||
|
||||
if ($this->buffer !== '') {
|
||||
// this is the first element in the queue and the buffer is filled => trigger write procedure
|
||||
$this->write('');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getBuffer()
|
||||
{
|
||||
return $this->buffer;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013 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.
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"name": "clue/stdio-react",
|
||||
"description": "Async, event-driven console input & output (STDIN, STDOUT) for truly interactive CLI applications, built on top of ReactPHP",
|
||||
"keywords": ["stdio", "stdin", "stdout", "interactive", "CLI", "readline", "autocomplete", "autocompletion", "history", "ReactPHP", "async"],
|
||||
"homepage": "https://github.com/clue/reactphp-stdio",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Christian Lück",
|
||||
"email": "christian@clue.engineering"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=5.3",
|
||||
"clue/term-react": "^1.0 || ^0.1.1",
|
||||
"clue/utf8-react": "^1.0 || ^0.1",
|
||||
"react/event-loop": "^1.2",
|
||||
"react/stream": "^1.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"clue/arguments": "^2.0",
|
||||
"clue/commander": "^1.2",
|
||||
"phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-mbstring": "Using ext-mbstring should provide slightly better performance for handling I/O"
|
||||
},
|
||||
"config": {
|
||||
"sort-packages": true
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": { "Clue\\React\\Stdio\\": "src/" }
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": { "Clue\\Tests\\React\\Stdio\\": "tests/" }
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,630 @@
|
|||
<?php
|
||||
|
||||
namespace Clue\React\Stdio;
|
||||
|
||||
use Evenement\EventEmitter;
|
||||
use React\EventLoop\LoopInterface;
|
||||
use React\Stream\DuplexStreamInterface;
|
||||
use React\Stream\ReadableResourceStream;
|
||||
use React\Stream\ReadableStreamInterface;
|
||||
use React\Stream\Util;
|
||||
use React\Stream\WritableResourceStream;
|
||||
use React\Stream\WritableStreamInterface;
|
||||
|
||||
class Stdio extends EventEmitter implements DuplexStreamInterface
|
||||
{
|
||||
private $input;
|
||||
private $output;
|
||||
private $readline;
|
||||
|
||||
private $ending = false;
|
||||
private $closed = false;
|
||||
private $incompleteLine = '';
|
||||
private $originalTtyMode = null;
|
||||
|
||||
/**
|
||||
*
|
||||
* This class takes an optional `LoopInterface|null $loop` parameter that can be used to
|
||||
* pass the event loop instance to use for this object. You can use a `null` value
|
||||
* here in order to use the [default loop](https://github.com/reactphp/event-loop#loop).
|
||||
* This value SHOULD NOT be given unless you're sure you want to explicitly use a
|
||||
* given event loop instance.
|
||||
*
|
||||
* @param ?LoopInterface $loop
|
||||
* @param ?ReadableStreamInterface $input
|
||||
* @param ?WritableStreamInterface $output
|
||||
* @param ?Readline $readline
|
||||
*/
|
||||
public function __construct(LoopInterface $loop = null, ReadableStreamInterface $input = null, WritableStreamInterface $output = null, Readline $readline = null)
|
||||
{
|
||||
if ($input === null) {
|
||||
$input = $this->createStdin($loop); // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
if ($output === null) {
|
||||
$output = $this->createStdout($loop); // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
if ($readline === null) {
|
||||
$readline = new Readline($input, $output, $this);
|
||||
}
|
||||
|
||||
$this->input = $input;
|
||||
$this->output = $output;
|
||||
$this->readline = $readline;
|
||||
|
||||
$that = $this;
|
||||
|
||||
// readline data emits a new line
|
||||
$incomplete =& $this->incompleteLine;
|
||||
$this->readline->on('data', function($line) use ($that, &$incomplete) {
|
||||
// readline emits a new line on enter, so start with a blank line
|
||||
$incomplete = '';
|
||||
$that->emit('data', array($line));
|
||||
});
|
||||
|
||||
// handle all input events (readline forwards all input events)
|
||||
$this->readline->on('error', array($this, 'handleError'));
|
||||
$this->readline->on('end', array($this, 'handleEnd'));
|
||||
$this->readline->on('close', array($this, 'handleCloseInput'));
|
||||
|
||||
// handle all output events
|
||||
$this->output->on('error', array($this, 'handleError'));
|
||||
$this->output->on('close', array($this, 'handleCloseOutput'));
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
$this->restoreTtyMode();
|
||||
}
|
||||
|
||||
public function pause()
|
||||
{
|
||||
$this->input->pause();
|
||||
}
|
||||
|
||||
public function resume()
|
||||
{
|
||||
$this->input->resume();
|
||||
}
|
||||
|
||||
public function isReadable()
|
||||
{
|
||||
return $this->input->isReadable();
|
||||
}
|
||||
|
||||
public function isWritable()
|
||||
{
|
||||
return $this->output->isWritable();
|
||||
}
|
||||
|
||||
public function pipe(WritableStreamInterface $dest, array $options = array())
|
||||
{
|
||||
Util::pipe($this, $dest, $options);
|
||||
|
||||
return $dest;
|
||||
}
|
||||
|
||||
public function write($data)
|
||||
{
|
||||
// return false if already ended, return true if writing empty string
|
||||
if ($this->ending || $data === '') {
|
||||
return !$this->ending;
|
||||
}
|
||||
|
||||
$out = $data;
|
||||
|
||||
$lastNewline = strrpos($data, "\n");
|
||||
|
||||
$restoreReadline = false;
|
||||
|
||||
if ($this->incompleteLine !== '') {
|
||||
// the last write did not end with a newline => append to existing row
|
||||
|
||||
// move one line up and move cursor to last position before writing data
|
||||
$out = "\033[A" . "\r\033[" . $this->width($this->incompleteLine) . "C" . $out;
|
||||
|
||||
// data contains a newline, so this will overwrite the readline prompt
|
||||
if ($lastNewline !== false) {
|
||||
// move cursor to beginning of readline prompt and clear line
|
||||
// clearing is important because $data may not overwrite the whole line
|
||||
$out = "\r\033[K" . $out;
|
||||
|
||||
// make sure to restore readline after this output
|
||||
$restoreReadline = true;
|
||||
}
|
||||
} else {
|
||||
// here, we're writing to a new line => overwrite readline prompt
|
||||
|
||||
// move cursor to beginning of readline prompt and clear line
|
||||
$out = "\r\033[K" . $out;
|
||||
|
||||
// we always overwrite the readline prompt, so restore it on next line
|
||||
$restoreReadline = true;
|
||||
}
|
||||
|
||||
// following write will have have to append to this line if it does not end with a newline
|
||||
$endsWithNewline = substr($data, -1) === "\n";
|
||||
|
||||
if ($endsWithNewline) {
|
||||
// line ends with newline, so this is line is considered complete
|
||||
$this->incompleteLine = '';
|
||||
} else {
|
||||
// always end data with newline in order to append readline on next line
|
||||
$out .= "\n";
|
||||
|
||||
if ($lastNewline === false) {
|
||||
// contains no newline at all, everything is incomplete
|
||||
$this->incompleteLine .= $data;
|
||||
} else {
|
||||
// contains a newline, everything behind it is incomplete
|
||||
$this->incompleteLine = (string)substr($data, $lastNewline + 1);
|
||||
}
|
||||
}
|
||||
|
||||
if ($restoreReadline) {
|
||||
// write output and restore original readline prompt and line buffer
|
||||
return $this->output->write($out . $this->readline->getDrawString());
|
||||
} else {
|
||||
// restore original cursor position in readline prompt
|
||||
$pos = $this->width($this->readline->getPrompt()) + $this->readline->getCursorCell();
|
||||
if ($pos !== 0) {
|
||||
// we always start at beginning of line, move right by X
|
||||
$out .= "\033[" . $pos . "C";
|
||||
}
|
||||
|
||||
// write to actual output stream
|
||||
return $this->output->write($out);
|
||||
}
|
||||
}
|
||||
|
||||
public function end($data = null)
|
||||
{
|
||||
if ($this->ending) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($data !== null) {
|
||||
$this->write($data);
|
||||
}
|
||||
|
||||
$this->ending = true;
|
||||
|
||||
// clear readline output, close input and end output
|
||||
$this->readline->setInput('')->setPrompt('');
|
||||
$this->input->close();
|
||||
$this->output->end();
|
||||
}
|
||||
|
||||
public function close()
|
||||
{
|
||||
if ($this->closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->ending = true;
|
||||
$this->closed = true;
|
||||
|
||||
$this->input->close();
|
||||
$this->output->close();
|
||||
|
||||
$this->emit('close');
|
||||
$this->removeAllListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* @return Readline
|
||||
*/
|
||||
public function getReadline()
|
||||
{
|
||||
return $this->readline;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* prompt to prepend to input line
|
||||
*
|
||||
* Will redraw the current input prompt with the current input buffer.
|
||||
*
|
||||
* @param string $prompt
|
||||
* @return void
|
||||
*/
|
||||
public function setPrompt($prompt)
|
||||
{
|
||||
$this->readline->setPrompt($prompt);
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the prompt to prepend to input line
|
||||
*
|
||||
* @return string
|
||||
* @see self::setPrompt()
|
||||
*/
|
||||
public function getPrompt()
|
||||
{
|
||||
return $this->readline->getPrompt();
|
||||
}
|
||||
|
||||
/**
|
||||
* sets whether/how to echo text input
|
||||
*
|
||||
* The default setting is `true`, which means that every character will be
|
||||
* echo'ed as-is, i.e. you can see what you're typing.
|
||||
* For example: Typing "test" shows "test".
|
||||
*
|
||||
* You can turn this off by supplying `false`, which means that *nothing*
|
||||
* will be echo'ed while you're typing. This could be a good idea for
|
||||
* password prompts. Note that this could be confusing for users, so using
|
||||
* a character replacement as following is often preferred.
|
||||
* For example: Typing "test" shows "" (nothing).
|
||||
*
|
||||
* Alternative, you can supply a single character replacement character
|
||||
* that will be echo'ed for each character in the text input. This could
|
||||
* be a good idea for password prompts, where an asterisk character ("*")
|
||||
* is often used to indicate typing activity and password length.
|
||||
* For example: Typing "test" shows "****" (with asterisk replacement)
|
||||
*
|
||||
* Changing this setting will redraw the current prompt and echo the current
|
||||
* input buffer according to the new setting.
|
||||
*
|
||||
* @param boolean|string $echo echo can be turned on (boolean true) or off (boolean true), or you can supply a single character replacement string
|
||||
* @return void
|
||||
*/
|
||||
public function setEcho($echo)
|
||||
{
|
||||
$this->readline->setEcho($echo);
|
||||
}
|
||||
|
||||
/**
|
||||
* whether or not to support moving cursor left and right
|
||||
*
|
||||
* switching cursor support moves the cursor to the end of the current
|
||||
* input buffer (if any).
|
||||
*
|
||||
* @param boolean $move
|
||||
* @return void
|
||||
*/
|
||||
public function setMove($move)
|
||||
{
|
||||
$this->readline->setMove($move);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets current cursor position measured in number of text characters.
|
||||
*
|
||||
* Note that the number of text characters doesn't necessarily reflect the
|
||||
* number of monospace cells occupied by the text characters. If you want
|
||||
* to know the latter, use `self::getCursorCell()` instead.
|
||||
*
|
||||
* @return int
|
||||
* @see self::getCursorCell() to get the position measured in monospace cells
|
||||
* @see self::moveCursorTo() to move the cursor to a given character position
|
||||
* @see self::moveCursorBy() to move the cursor by given number of characters
|
||||
* @see self::setMove() to toggle whether the user can move the cursor position
|
||||
*/
|
||||
public function getCursorPosition()
|
||||
{
|
||||
return $this->readline->getCursorPosition();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets current cursor position measured in monospace cells.
|
||||
*
|
||||
* Note that the cell position doesn't necessarily reflect the number of
|
||||
* text characters. If you want to know the latter, use
|
||||
* `self::getCursorPosition()` instead.
|
||||
*
|
||||
* Most "normal" characters occupy a single monospace cell, i.e. the ASCII
|
||||
* sequence for "A" requires a single cell, as do most UTF-8 sequences
|
||||
* like "Ä".
|
||||
*
|
||||
* However, there are a number of code points that do not require a cell
|
||||
* (i.e. invisible surrogates) or require two cells (e.g. some asian glyphs).
|
||||
*
|
||||
* Also note that this takes the echo mode into account, i.e. the cursor is
|
||||
* always at position zero if echo is off. If using a custom echo character
|
||||
* (like asterisk), it will take its width into account instead of the actual
|
||||
* input characters.
|
||||
*
|
||||
* @return int
|
||||
* @see self::getCursorPosition() to get current cursor position measured in characters
|
||||
* @see self::moveCursorTo() to move the cursor to a given character position
|
||||
* @see self::moveCursorBy() to move the cursor by given number of characters
|
||||
* @see self::setMove() to toggle whether the user can move the cursor position
|
||||
* @see self::setEcho()
|
||||
*/
|
||||
public function getCursorCell()
|
||||
{
|
||||
return $this->readline->getCursorCell();
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves cursor to right by $n chars (or left if $n is negative).
|
||||
*
|
||||
* Zero value or values out of range (exceeding current input buffer) are
|
||||
* simply ignored.
|
||||
*
|
||||
* Will redraw() the readline only if the visible cell position changes,
|
||||
* see `self::getCursorCell()` for more details.
|
||||
*
|
||||
* @param int $n
|
||||
* @return void
|
||||
*/
|
||||
public function moveCursorBy($n)
|
||||
{
|
||||
$this->readline->moveCursorBy($n);
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves cursor to given position in current line buffer.
|
||||
*
|
||||
* Values out of range (exceeding current input buffer) are simply ignored.
|
||||
*
|
||||
* Will redraw() the readline only if the visible cell position changes,
|
||||
* see `self::getCursorCell()` for more details.
|
||||
*
|
||||
* @param int $n
|
||||
* @return void
|
||||
*/
|
||||
public function moveCursorTo($n)
|
||||
{
|
||||
$this->readline->moveCursorTo($n);
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends the given input to the current text input buffer at the current position
|
||||
*
|
||||
* This moves the cursor accordingly to the number of characters added.
|
||||
*
|
||||
* @param string $input
|
||||
* @return void
|
||||
*/
|
||||
public function addInput($input)
|
||||
{
|
||||
$this->readline->addInput($input);
|
||||
}
|
||||
|
||||
/**
|
||||
* set current text input buffer
|
||||
*
|
||||
* this moves the cursor to the end of the current
|
||||
* input buffer (if any).
|
||||
*
|
||||
* @param string $input
|
||||
* @return void
|
||||
*/
|
||||
public function setInput($input)
|
||||
{
|
||||
$this->readline->setInput($input);
|
||||
}
|
||||
|
||||
/**
|
||||
* get current text input buffer
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getInput()
|
||||
{
|
||||
return $this->readline->getInput();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new line to the (bottom position of the) history list
|
||||
*
|
||||
* @param string $line
|
||||
* @return void
|
||||
*/
|
||||
public function addHistory($line)
|
||||
{
|
||||
$this->readline->addHistory($line);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the complete history list
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function clearHistory()
|
||||
{
|
||||
$this->readline->clearHistory();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array with all lines in the history
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function listHistory()
|
||||
{
|
||||
return $this->readline->listHistory();
|
||||
}
|
||||
|
||||
/**
|
||||
* Limits the history to a maximum of N entries and truncates the current history list accordingly
|
||||
*
|
||||
* @param int|null $limit
|
||||
* @return void
|
||||
*/
|
||||
public function limitHistory($limit)
|
||||
{
|
||||
$this->readline->limitHistory($limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* set autocompletion handler to use
|
||||
*
|
||||
* The autocomplete handler will be called whenever the user hits the TAB
|
||||
* key.
|
||||
*
|
||||
* @param callable|null $autocomplete
|
||||
* @return void
|
||||
* @throws \InvalidArgumentException if the given callable is invalid
|
||||
*/
|
||||
public function setAutocomplete($autocomplete)
|
||||
{
|
||||
$this->readline->setAutocomplete($autocomplete);
|
||||
}
|
||||
|
||||
/**
|
||||
* whether or not to emit a audible/visible BELL signal when using a disabled function
|
||||
*
|
||||
* By default, this class will emit a BELL signal when using a disable function,
|
||||
* such as using the <kbd>left</kbd> or <kbd>backspace</kbd> keys when
|
||||
* already at the beginning of the line.
|
||||
*
|
||||
* Whether or not the BELL is audible/visible depends on the termin and its
|
||||
* settings, i.e. some terminals may "beep" or flash the screen or emit a
|
||||
* short vibration.
|
||||
*
|
||||
* @param bool $bell
|
||||
* @return void
|
||||
*/
|
||||
public function setBell($bell)
|
||||
{
|
||||
$this->readline->setBell($bell);
|
||||
}
|
||||
|
||||
private function width($str)
|
||||
{
|
||||
return $this->readline->strwidth($str) - 2 * substr_count($str, "\x08");
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
public function handleError(\Exception $e)
|
||||
{
|
||||
$this->emit('error', array($e));
|
||||
$this->close();
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
public function handleEnd()
|
||||
{
|
||||
$this->emit('end');
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
public function handleCloseInput()
|
||||
{
|
||||
$this->restoreTtyMode();
|
||||
|
||||
if (!$this->output->isWritable()) {
|
||||
$this->close();
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
public function handleCloseOutput()
|
||||
{
|
||||
if (!$this->input->isReadable()) {
|
||||
$this->close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore this is covered by functional tests with/without ext-readline
|
||||
*/
|
||||
private function restoreTtyMode()
|
||||
{
|
||||
if (function_exists('readline_callback_handler_remove')) {
|
||||
// remove dummy readline handler to turn to default input mode
|
||||
readline_callback_handler_remove();
|
||||
} elseif ($this->originalTtyMode !== null && is_resource(STDIN) && $this->isTty()) {
|
||||
// Reset stty so it behaves normally again
|
||||
shell_exec('stty ' . escapeshellarg($this->originalTtyMode));
|
||||
$this->originalTtyMode = null;
|
||||
}
|
||||
|
||||
// restore blocking mode so following programs behave normally
|
||||
if (defined('STDIN') && is_resource(STDIN)) {
|
||||
stream_set_blocking(STDIN, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ?LoopInterface $loop
|
||||
* @return ReadableStreamInterface
|
||||
* @codeCoverageIgnore this is covered by functional tests with/without ext-readline
|
||||
*/
|
||||
private function createStdin(LoopInterface $loop = null)
|
||||
{
|
||||
// STDIN not defined ("php -a") or already closed (`fclose(STDIN)`)
|
||||
// also support starting program with closed STDIN ("example.php 0<&-")
|
||||
// the stream is a valid resource and is not EOF, but fstat fails
|
||||
if (!defined('STDIN') || !is_resource(STDIN) || fstat(STDIN) === false) {
|
||||
$stream = new ReadableResourceStream(fopen('php://memory', 'r'), $loop);
|
||||
$stream->close();
|
||||
return $stream;
|
||||
}
|
||||
|
||||
$stream = new ReadableResourceStream(STDIN, $loop);
|
||||
|
||||
if (function_exists('readline_callback_handler_install')) {
|
||||
// Prefer `ext-readline` to install dummy handler to turn on raw input mode.
|
||||
// We will never actually feed the readline handler and instead
|
||||
// handle all input in our `Readline` implementation.
|
||||
readline_callback_handler_install('', function () { });
|
||||
return $stream;
|
||||
}
|
||||
|
||||
if ($this->isTty()) {
|
||||
$this->originalTtyMode = rtrim(shell_exec('stty -g'), PHP_EOL);
|
||||
|
||||
// Disable icanon (so we can fread each keypress) and echo (we'll do echoing here instead)
|
||||
shell_exec('stty -icanon -echo');
|
||||
}
|
||||
|
||||
// register shutdown function to restore TTY mode in case of unclean shutdown (uncaught exception)
|
||||
// this will not trigger on SIGKILL etc., but the terminal should take care of this
|
||||
register_shutdown_function(array($this, 'close'));
|
||||
|
||||
return $stream;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ?LoopInterface $loop
|
||||
* @return WritableStreamInterface
|
||||
* @codeCoverageIgnore this is covered by functional tests
|
||||
*/
|
||||
private function createStdout(LoopInterface $loop = null)
|
||||
{
|
||||
// STDOUT not defined ("php -a") or already closed (`fclose(STDOUT)`)
|
||||
// also support starting program with closed STDOUT ("example.php >&-")
|
||||
// the stream is a valid resource and is not EOF, but fstat fails
|
||||
if (!defined('STDOUT') || !is_resource(STDOUT) || fstat(STDOUT) === false) {
|
||||
$output = new WritableResourceStream(fopen('php://memory', 'r+'), $loop);
|
||||
$output->close();
|
||||
} else {
|
||||
$output = new WritableResourceStream(STDOUT, $loop);
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
private function isTty()
|
||||
{
|
||||
if (PHP_VERSION_ID >= 70200) {
|
||||
// Prefer `stream_isatty()` (available as of PHP 7.2 only)
|
||||
return stream_isatty(STDIN);
|
||||
} elseif (function_exists('posix_isatty')) {
|
||||
// Otherwise use `posix_isatty` if available (requires `ext-posix`)
|
||||
return posix_isatty(STDIN);
|
||||
}
|
||||
|
||||
// otherwise try to guess based on stat file mode and device major number
|
||||
// Must be special character device: ($mode & S_IFMT) === S_IFCHR
|
||||
// And device major number must be allocated to TTYs (2-5 and 128-143)
|
||||
// For what it's worth, checking for device gid 5 (tty) is less reliable.
|
||||
// @link http://man7.org/linux/man-pages/man7/inode.7.html
|
||||
// @link https://www.kernel.org/doc/html/v4.11/admin-guide/devices.html#terminal-devices
|
||||
$stat = fstat(STDIN);
|
||||
$mode = isset($stat['mode']) ? ($stat['mode'] & 0170000) : 0;
|
||||
$major = isset($stat['dev']) ? (($stat['dev'] >> 8) & 0xff) : 0;
|
||||
|
||||
return ($mode === 0020000 && $major >= 2 && $major <= 143 && ($major <=5 || $major >= 128));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 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.
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"name": "clue/term-react",
|
||||
"description": "Streaming terminal emulator, built on top of ReactPHP.",
|
||||
"keywords": ["terminal", "control codes", "xterm", "ANSI", "ASCII", "VT100", "csi", "osc", "apc", "dps", "pm", "C1", "C0", "streaming", "ReactPHP"],
|
||||
"homepage": "https://github.com/clue/reactphp-term",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Christian Lück",
|
||||
"email": "christian@clue.engineering"
|
||||
}
|
||||
],
|
||||
"autoload": {
|
||||
"psr-4": { "Clue\\React\\Term\\": "src/" }
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": { "Clue\\Tests\\React\\Term\\": "tests/" }
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3",
|
||||
"react/stream": "^1.0 || ^0.7"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^9.3 || ^5.7 || ^4.8",
|
||||
"react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,223 @@
|
|||
<?php
|
||||
|
||||
namespace Clue\React\Term;
|
||||
|
||||
use Evenement\EventEmitter;
|
||||
use React\Stream\ReadableStreamInterface;
|
||||
use React\Stream\WritableStreamInterface;
|
||||
use React\Stream\Util;
|
||||
|
||||
class ControlCodeParser extends EventEmitter implements ReadableStreamInterface
|
||||
{
|
||||
private $input;
|
||||
private $closed = false;
|
||||
private $buffer = '';
|
||||
|
||||
/**
|
||||
* we know about the following C1 types (7 bit only)
|
||||
*
|
||||
* followed by "[" means it's CSI (Control Sequence Introducer)
|
||||
* followed by "]" means it's OSC (Operating System Controls)
|
||||
* followed by "_" means it's APC (Application Program-Control)
|
||||
* followed by "P" means it's DPS (Device-Control string)
|
||||
* followed by "^" means it's PM (Privacy Message)
|
||||
*
|
||||
* Each of these will be parsed until the sequence ends and then emitted
|
||||
* under their respective name.
|
||||
*
|
||||
* All other C1 types will be emitted under the "c1" name without any
|
||||
* further processing.
|
||||
*
|
||||
* C1 types in 8 bit are currently not supported, as they require special
|
||||
* care with regards to whether UTF-8 mode is enabled. So far this has
|
||||
* turned out to be a non-issue because most terminal emulators *accept*
|
||||
* boths formats, but usually *send* in 7 bit mode exclusively.
|
||||
*/
|
||||
private $types = array(
|
||||
'[' => 'csi',
|
||||
']' => 'osc',
|
||||
'_' => 'apc',
|
||||
'P' => 'dps',
|
||||
'^' => 'pm',
|
||||
);
|
||||
|
||||
public function __construct(ReadableStreamInterface $input)
|
||||
{
|
||||
$this->input = $input;
|
||||
|
||||
if (!$this->input->isReadable()) {
|
||||
return $this->close();
|
||||
}
|
||||
|
||||
$this->input->on('data', array($this, 'handleData'));
|
||||
$this->input->on('end', array($this, 'handleEnd'));
|
||||
$this->input->on('error', array($this, 'handleError'));
|
||||
$this->input->on('close', array($this, 'close'));
|
||||
}
|
||||
|
||||
public function isReadable()
|
||||
{
|
||||
return !$this->closed && $this->input->isReadable();
|
||||
}
|
||||
|
||||
public function pause()
|
||||
{
|
||||
$this->input->pause();
|
||||
}
|
||||
|
||||
public function resume()
|
||||
{
|
||||
$this->input->resume();
|
||||
}
|
||||
|
||||
public function pipe(WritableStreamInterface $dest, array $options = array())
|
||||
{
|
||||
Util::pipe($this, $dest, $options);
|
||||
|
||||
return $dest;
|
||||
}
|
||||
|
||||
public function close()
|
||||
{
|
||||
if ($this->closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->closed = true;
|
||||
$this->buffer = '';
|
||||
|
||||
$this->input->close();
|
||||
|
||||
$this->emit('close');
|
||||
$this->removeAllListeners();
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
public function handleData($data)
|
||||
{
|
||||
$this->buffer .= $data;
|
||||
|
||||
while ($this->buffer !== '') {
|
||||
// search for first control character (C0 and DEL)
|
||||
$c0 = false;
|
||||
for ($i = 0; isset($this->buffer[$i]); ++$i) {
|
||||
$code = ord($this->buffer[$i]);
|
||||
if ($code < 0x20 || $code === 0x7F) {
|
||||
$c0 = $i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// no C0 found, emit whole buffer as data
|
||||
if ($c0 === false) {
|
||||
$data = $this->buffer;
|
||||
$this->buffer = '';
|
||||
|
||||
$this->emit('data', array($data));
|
||||
return;
|
||||
}
|
||||
|
||||
// C0 found somewhere inbetween, emit everything before C0 as data
|
||||
if ($c0 !== 0) {
|
||||
$data = substr($this->buffer, 0, $c0);
|
||||
$this->buffer = substr($this->buffer, $c0);
|
||||
|
||||
$this->emit('data', array($data));
|
||||
continue;
|
||||
}
|
||||
|
||||
// C0 is now at start of buffer
|
||||
// check if this is a normal C0 code or an ESC (\x1B = \033)
|
||||
// normal C0 will be emitted, ESC will be parsed further
|
||||
if ($this->buffer[0] !== "\x1B") {
|
||||
$data = $this->buffer[0];
|
||||
$this->buffer = (string)substr($this->buffer, 1);
|
||||
|
||||
$this->emit('c0', array($data));
|
||||
continue;
|
||||
}
|
||||
|
||||
// check following byte to determine type
|
||||
if (!isset($this->buffer[1])) {
|
||||
// type currently unknown, wait for next data chunk
|
||||
break;
|
||||
}
|
||||
|
||||
// if this is an unknown type, just emit as "c1" without further parsing
|
||||
if (!isset($this->types[$this->buffer[1]])) {
|
||||
$data = substr($this->buffer, 0, 2);
|
||||
$this->buffer = (string)substr($this->buffer, 2);
|
||||
|
||||
$this->emit('c1', array($data));
|
||||
continue;
|
||||
}
|
||||
|
||||
// this is known type, check for the sequence end
|
||||
$type = $this->types[$this->buffer[1]];
|
||||
$found = false;
|
||||
|
||||
if ($type === 'csi') {
|
||||
// CSI is now at the start of the buffer, search final character
|
||||
for ($i = 2; isset($this->buffer[$i]); ++$i) {
|
||||
$code = ord($this->buffer[$i]);
|
||||
|
||||
// final character between \x40-\x7E
|
||||
if ($code >= 64 && $code <= 126) {
|
||||
$data = substr($this->buffer, 0, $i + 1);
|
||||
$this->buffer = (string)substr($this->buffer, $i + 1);
|
||||
|
||||
$this->emit($type, array($data));
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// all other types are terminated by ST
|
||||
// only OSC can also be terminted by BEL (whichever comes first)
|
||||
$st = strpos($this->buffer, "\x1B\\");
|
||||
$bel = ($type === 'osc') ? strpos($this->buffer, "\x07") : false;
|
||||
|
||||
if ($st !== false && ($bel === false || $bel > $st)) {
|
||||
// ST comes before BEL or no BEL found
|
||||
$data = substr($this->buffer, 0, $st + 2);
|
||||
$this->buffer = (string)substr($this->buffer, $st + 2);
|
||||
|
||||
$this->emit($type, array($data));
|
||||
$found = true;
|
||||
} elseif ($bel !== false) {
|
||||
// BEL comes before ST or no ST found
|
||||
$data = substr($this->buffer, 0, $bel + 1);
|
||||
$this->buffer = (string)substr($this->buffer, $bel + 1);
|
||||
|
||||
$this->emit($type, array($data));
|
||||
$found = true;
|
||||
}
|
||||
}
|
||||
|
||||
// no final character found => wait for next data chunk
|
||||
if (!$found) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
public function handleEnd()
|
||||
{
|
||||
if (!$this->closed) {
|
||||
if ($this->buffer === '') {
|
||||
$this->emit('end');
|
||||
} else {
|
||||
$this->emit('error', array(new \RuntimeException('Stream ended with incomplete control code sequence in buffer')));
|
||||
}
|
||||
$this->close();
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
public function handleError(\Exception $e)
|
||||
{
|
||||
$this->emit('error', array($e));
|
||||
$this->close();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 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.
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"name": "clue/utf8-react",
|
||||
"description": "Streaming UTF-8 parser, built on top of ReactPHP.",
|
||||
"keywords": ["UTF-8", "utf8", "unicode", "streaming", "ReactPHP"],
|
||||
"homepage": "https://github.com/clue/reactphp-utf8",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Christian Lück",
|
||||
"email": "christian@clue.engineering"
|
||||
}
|
||||
],
|
||||
"autoload": {
|
||||
"psr-4": { "Clue\\React\\Utf8\\": "src/" }
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": { "Clue\\Tests\\React\\Utf8\\": "tests/" }
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3",
|
||||
"react/stream": "^1.0 || ^0.7 || ^0.6 || ^0.5 || ^0.4 || ^0.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^9.3 ||^5.7 || ^4.8",
|
||||
"react/stream": "^1.0 || ^0.7"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,174 @@
|
|||
<?php
|
||||
|
||||
namespace Clue\React\Utf8;
|
||||
|
||||
use Evenement\EventEmitter;
|
||||
use React\Stream\ReadableStreamInterface;
|
||||
use React\Stream\WritableStreamInterface;
|
||||
use React\Stream\Util;
|
||||
|
||||
/**
|
||||
* forwards only complete UTF-8 sequences
|
||||
*/
|
||||
class Sequencer extends EventEmitter implements ReadableStreamInterface
|
||||
{
|
||||
private $input;
|
||||
private $invalid;
|
||||
|
||||
private $buffer = '';
|
||||
private $closed = false;
|
||||
|
||||
public function __construct(ReadableStreamInterface $input, $replacementCharacter = '?')
|
||||
{
|
||||
$this->input = $input;
|
||||
$this->invalid = $replacementCharacter;
|
||||
|
||||
if (!$input->isReadable()) {
|
||||
return $this->close();
|
||||
}
|
||||
|
||||
$this->input->on('data', array($this, 'handleData'));
|
||||
$this->input->on('end', array($this, 'handleEnd'));
|
||||
$this->input->on('error', array($this, 'handleError'));
|
||||
$this->input->on('close', array($this, 'close'));
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
public function handleData($data)
|
||||
{
|
||||
$this->buffer .= $data;
|
||||
$len = strlen($this->buffer);
|
||||
|
||||
$sequence = '';
|
||||
$expect = 0;
|
||||
$out = '';
|
||||
|
||||
for ($i = 0; $i < $len; ++$i) {
|
||||
$char = $this->buffer[$i];
|
||||
$code = ord($char);
|
||||
|
||||
if ($code & 128) {
|
||||
// multi-byte sequence
|
||||
if ($code & 64) {
|
||||
// this is the start of a sequence
|
||||
|
||||
// unexpected start of sequence because already within sequence
|
||||
if ($expect !== 0) {
|
||||
$out .= str_repeat($this->invalid, strlen($sequence));
|
||||
$sequence = '';
|
||||
}
|
||||
|
||||
$sequence = $char;
|
||||
$expect = 2;
|
||||
|
||||
if ($code & 32) {
|
||||
++$expect;
|
||||
if ($code & 16) {
|
||||
++$expect;
|
||||
|
||||
if ($code & 8) {
|
||||
// invalid sequence start length
|
||||
$out .= $this->invalid;
|
||||
$sequence = '';
|
||||
$expect = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// this is a follow-up byte in a sequence
|
||||
if ($expect === 0) {
|
||||
// we're not within a sequence in first place
|
||||
$out .= $this->invalid;
|
||||
} else {
|
||||
// valid following byte in sequence
|
||||
$sequence .= $char;
|
||||
|
||||
// sequence reached expected length => add to output
|
||||
if (strlen($sequence) === $expect) {
|
||||
$out .= $sequence;
|
||||
$sequence = '';
|
||||
$expect = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// simple ASCII character found
|
||||
|
||||
// unexpected because already within sequence
|
||||
if ($expect !== 0) {
|
||||
$out .= str_repeat($this->invalid, strlen($sequence));
|
||||
$sequence = '';
|
||||
$expect = 0;
|
||||
}
|
||||
|
||||
$out .= $char;
|
||||
}
|
||||
}
|
||||
|
||||
if ($out !== '') {
|
||||
$this->buffer = substr($this->buffer, strlen($out));
|
||||
|
||||
$this->emit('data', array($out));
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
public function handleEnd()
|
||||
{
|
||||
if ($this->buffer !== '' && $this->invalid !== '') {
|
||||
$data = str_repeat($this->invalid, strlen($this->buffer));
|
||||
$this->buffer = '';
|
||||
|
||||
$this->emit('data', array($data));
|
||||
}
|
||||
|
||||
if (!$this->closed) {
|
||||
$this->emit('end');
|
||||
$this->close();
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
public function handleError(\Exception $error)
|
||||
{
|
||||
$this->emit('error', array($error));
|
||||
$this->close();
|
||||
}
|
||||
|
||||
public function isReadable()
|
||||
{
|
||||
return !$this->closed && $this->input->isReadable();
|
||||
}
|
||||
|
||||
public function close()
|
||||
{
|
||||
if ($this->closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->closed = true;
|
||||
$this->buffer = '';
|
||||
|
||||
$this->input->close();
|
||||
|
||||
$this->emit('close');
|
||||
$this->removeAllListeners();
|
||||
}
|
||||
|
||||
public function pause()
|
||||
{
|
||||
$this->input->pause();
|
||||
}
|
||||
|
||||
public function resume()
|
||||
{
|
||||
$this->input->resume();
|
||||
}
|
||||
|
||||
public function pipe(WritableStreamInterface $dest, array $options = array())
|
||||
{
|
||||
Util::pipe($this, $dest, $options);
|
||||
|
||||
return $dest;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"name": "jquery",
|
||||
"version": "3.5.1",
|
||||
"description": "jQuery component",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"jquery",
|
||||
"component"
|
||||
],
|
||||
"main": "jquery.js",
|
||||
"ignore": [
|
||||
"component.json",
|
||||
"package.json",
|
||||
"composer.json"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"name": "jquery",
|
||||
"repo": "components/jquery",
|
||||
"version": "3.5.1",
|
||||
"description": "jQuery component",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"jquery",
|
||||
"component"
|
||||
],
|
||||
"main": "jquery.js",
|
||||
"scripts": [
|
||||
"jquery.js",
|
||||
"jquery.min.js",
|
||||
"jquery.slim.js",
|
||||
"jquery.slim.min.js"
|
||||
],
|
||||
"files": [
|
||||
"jquery.min.map",
|
||||
"jquery.slim.min.map"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"name": "components/jquery",
|
||||
"description": "jQuery JavaScript Library",
|
||||
"type": "component",
|
||||
"homepage": "http://jquery.com",
|
||||
"license": "MIT",
|
||||
"support": {
|
||||
"irc": "irc://irc.freenode.org/jquery",
|
||||
"issues": "https://github.com/jquery/jquery/issues",
|
||||
"forum": "http://forum.jquery.com",
|
||||
"wiki": "http://docs.jquery.com/",
|
||||
"source": "https://github.com/jquery/jquery"
|
||||
},
|
||||
"authors": [
|
||||
{
|
||||
"name": "JS Foundation and other contributors",
|
||||
"url": "https://github.com/jquery/jquery/blob/master/AUTHORS.txt"
|
||||
}
|
||||
],
|
||||
"extra": {
|
||||
"component": {
|
||||
"scripts": [
|
||||
"jquery.js"
|
||||
],
|
||||
"files": [
|
||||
"jquery.min.js",
|
||||
"jquery.min.map",
|
||||
"jquery.slim.js",
|
||||
"jquery.slim.min.js",
|
||||
"jquery.slim.min.map"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"name": "jquery",
|
||||
"description": "JavaScript library for DOM operations",
|
||||
"version": "3.5.1",
|
||||
"homepage": "http://jquery.com",
|
||||
"author": {
|
||||
"name": "jQuery Foundation and other contributors",
|
||||
"url": "https://github.com/jquery/jquery/blob/master/AUTHORS.txt"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/jquery/jquery.git"
|
||||
},
|
||||
"keywords": [
|
||||
"jquery",
|
||||
"javascript",
|
||||
"browser",
|
||||
"library"
|
||||
],
|
||||
"bugs": {
|
||||
"url": "https://github.com/jquery/jquery/issues"
|
||||
},
|
||||
"licenses": [
|
||||
{
|
||||
"type": "MIT",
|
||||
"url": "https://github.com/jquery/jquery/blob/master/LICENSE.txt"
|
||||
}
|
||||
],
|
||||
"spm": {
|
||||
"main": "jquery.js"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,579 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Composer.
|
||||
*
|
||||
* (c) Nils Adermann <naderman@naderman.de>
|
||||
* Jordi Boggiano <j.boggiano@seld.be>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Composer\Autoload;
|
||||
|
||||
/**
|
||||
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
|
||||
*
|
||||
* $loader = new \Composer\Autoload\ClassLoader();
|
||||
*
|
||||
* // register classes with namespaces
|
||||
* $loader->add('Symfony\Component', __DIR__.'/component');
|
||||
* $loader->add('Symfony', __DIR__.'/framework');
|
||||
*
|
||||
* // activate the autoloader
|
||||
* $loader->register();
|
||||
*
|
||||
* // to enable searching the include path (eg. for PEAR packages)
|
||||
* $loader->setUseIncludePath(true);
|
||||
*
|
||||
* In this example, if you try to use a class in the Symfony\Component
|
||||
* namespace or one of its children (Symfony\Component\Console for instance),
|
||||
* the autoloader will first look for the class under the component/
|
||||
* directory, and it will then fallback to the framework/ directory if not
|
||||
* found before giving up.
|
||||
*
|
||||
* This class is loosely based on the Symfony UniversalClassLoader.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Jordi Boggiano <j.boggiano@seld.be>
|
||||
* @see https://www.php-fig.org/psr/psr-0/
|
||||
* @see https://www.php-fig.org/psr/psr-4/
|
||||
*/
|
||||
class ClassLoader
|
||||
{
|
||||
/** @var \Closure(string):void */
|
||||
private static $includeFile;
|
||||
|
||||
/** @var string|null */
|
||||
private $vendorDir;
|
||||
|
||||
// PSR-4
|
||||
/**
|
||||
* @var array<string, array<string, int>>
|
||||
*/
|
||||
private $prefixLengthsPsr4 = array();
|
||||
/**
|
||||
* @var array<string, list<string>>
|
||||
*/
|
||||
private $prefixDirsPsr4 = array();
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
private $fallbackDirsPsr4 = array();
|
||||
|
||||
// PSR-0
|
||||
/**
|
||||
* List of PSR-0 prefixes
|
||||
*
|
||||
* Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
|
||||
*
|
||||
* @var array<string, array<string, list<string>>>
|
||||
*/
|
||||
private $prefixesPsr0 = array();
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
private $fallbackDirsPsr0 = array();
|
||||
|
||||
/** @var bool */
|
||||
private $useIncludePath = false;
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
private $classMap = array();
|
||||
|
||||
/** @var bool */
|
||||
private $classMapAuthoritative = false;
|
||||
|
||||
/**
|
||||
* @var array<string, bool>
|
||||
*/
|
||||
private $missingClasses = array();
|
||||
|
||||
/** @var string|null */
|
||||
private $apcuPrefix;
|
||||
|
||||
/**
|
||||
* @var array<string, self>
|
||||
*/
|
||||
private static $registeredLoaders = array();
|
||||
|
||||
/**
|
||||
* @param string|null $vendorDir
|
||||
*/
|
||||
public function __construct($vendorDir = null)
|
||||
{
|
||||
$this->vendorDir = $vendorDir;
|
||||
self::initializeIncludeClosure();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, list<string>>
|
||||
*/
|
||||
public function getPrefixes()
|
||||
{
|
||||
if (!empty($this->prefixesPsr0)) {
|
||||
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
|
||||
}
|
||||
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, list<string>>
|
||||
*/
|
||||
public function getPrefixesPsr4()
|
||||
{
|
||||
return $this->prefixDirsPsr4;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public function getFallbackDirs()
|
||||
{
|
||||
return $this->fallbackDirsPsr0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public function getFallbackDirsPsr4()
|
||||
{
|
||||
return $this->fallbackDirsPsr4;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string> Array of classname => path
|
||||
*/
|
||||
public function getClassMap()
|
||||
{
|
||||
return $this->classMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $classMap Class to filename map
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addClassMap(array $classMap)
|
||||
{
|
||||
if ($this->classMap) {
|
||||
$this->classMap = array_merge($this->classMap, $classMap);
|
||||
} else {
|
||||
$this->classMap = $classMap;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a set of PSR-0 directories for a given prefix, either
|
||||
* appending or prepending to the ones previously set for this prefix.
|
||||
*
|
||||
* @param string $prefix The prefix
|
||||
* @param list<string>|string $paths The PSR-0 root directories
|
||||
* @param bool $prepend Whether to prepend the directories
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add($prefix, $paths, $prepend = false)
|
||||
{
|
||||
$paths = (array) $paths;
|
||||
if (!$prefix) {
|
||||
if ($prepend) {
|
||||
$this->fallbackDirsPsr0 = array_merge(
|
||||
$paths,
|
||||
$this->fallbackDirsPsr0
|
||||
);
|
||||
} else {
|
||||
$this->fallbackDirsPsr0 = array_merge(
|
||||
$this->fallbackDirsPsr0,
|
||||
$paths
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$first = $prefix[0];
|
||||
if (!isset($this->prefixesPsr0[$first][$prefix])) {
|
||||
$this->prefixesPsr0[$first][$prefix] = $paths;
|
||||
|
||||
return;
|
||||
}
|
||||
if ($prepend) {
|
||||
$this->prefixesPsr0[$first][$prefix] = array_merge(
|
||||
$paths,
|
||||
$this->prefixesPsr0[$first][$prefix]
|
||||
);
|
||||
} else {
|
||||
$this->prefixesPsr0[$first][$prefix] = array_merge(
|
||||
$this->prefixesPsr0[$first][$prefix],
|
||||
$paths
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a set of PSR-4 directories for a given namespace, either
|
||||
* appending or prepending to the ones previously set for this namespace.
|
||||
*
|
||||
* @param string $prefix The prefix/namespace, with trailing '\\'
|
||||
* @param list<string>|string $paths The PSR-4 base directories
|
||||
* @param bool $prepend Whether to prepend the directories
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addPsr4($prefix, $paths, $prepend = false)
|
||||
{
|
||||
$paths = (array) $paths;
|
||||
if (!$prefix) {
|
||||
// Register directories for the root namespace.
|
||||
if ($prepend) {
|
||||
$this->fallbackDirsPsr4 = array_merge(
|
||||
$paths,
|
||||
$this->fallbackDirsPsr4
|
||||
);
|
||||
} else {
|
||||
$this->fallbackDirsPsr4 = array_merge(
|
||||
$this->fallbackDirsPsr4,
|
||||
$paths
|
||||
);
|
||||
}
|
||||
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
|
||||
// Register directories for a new namespace.
|
||||
$length = strlen($prefix);
|
||||
if ('\\' !== $prefix[$length - 1]) {
|
||||
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
|
||||
}
|
||||
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
|
||||
$this->prefixDirsPsr4[$prefix] = $paths;
|
||||
} elseif ($prepend) {
|
||||
// Prepend directories for an already registered namespace.
|
||||
$this->prefixDirsPsr4[$prefix] = array_merge(
|
||||
$paths,
|
||||
$this->prefixDirsPsr4[$prefix]
|
||||
);
|
||||
} else {
|
||||
// Append directories for an already registered namespace.
|
||||
$this->prefixDirsPsr4[$prefix] = array_merge(
|
||||
$this->prefixDirsPsr4[$prefix],
|
||||
$paths
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a set of PSR-0 directories for a given prefix,
|
||||
* replacing any others previously set for this prefix.
|
||||
*
|
||||
* @param string $prefix The prefix
|
||||
* @param list<string>|string $paths The PSR-0 base directories
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function set($prefix, $paths)
|
||||
{
|
||||
if (!$prefix) {
|
||||
$this->fallbackDirsPsr0 = (array) $paths;
|
||||
} else {
|
||||
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a set of PSR-4 directories for a given namespace,
|
||||
* replacing any others previously set for this namespace.
|
||||
*
|
||||
* @param string $prefix The prefix/namespace, with trailing '\\'
|
||||
* @param list<string>|string $paths The PSR-4 base directories
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setPsr4($prefix, $paths)
|
||||
{
|
||||
if (!$prefix) {
|
||||
$this->fallbackDirsPsr4 = (array) $paths;
|
||||
} else {
|
||||
$length = strlen($prefix);
|
||||
if ('\\' !== $prefix[$length - 1]) {
|
||||
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
|
||||
}
|
||||
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
|
||||
$this->prefixDirsPsr4[$prefix] = (array) $paths;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns on searching the include path for class files.
|
||||
*
|
||||
* @param bool $useIncludePath
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setUseIncludePath($useIncludePath)
|
||||
{
|
||||
$this->useIncludePath = $useIncludePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be used to check if the autoloader uses the include path to check
|
||||
* for classes.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getUseIncludePath()
|
||||
{
|
||||
return $this->useIncludePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns off searching the prefix and fallback directories for classes
|
||||
* that have not been registered with the class map.
|
||||
*
|
||||
* @param bool $classMapAuthoritative
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setClassMapAuthoritative($classMapAuthoritative)
|
||||
{
|
||||
$this->classMapAuthoritative = $classMapAuthoritative;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should class lookup fail if not found in the current class map?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isClassMapAuthoritative()
|
||||
{
|
||||
return $this->classMapAuthoritative;
|
||||
}
|
||||
|
||||
/**
|
||||
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
|
||||
*
|
||||
* @param string|null $apcuPrefix
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setApcuPrefix($apcuPrefix)
|
||||
{
|
||||
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The APCu prefix in use, or null if APCu caching is not enabled.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getApcuPrefix()
|
||||
{
|
||||
return $this->apcuPrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers this instance as an autoloader.
|
||||
*
|
||||
* @param bool $prepend Whether to prepend the autoloader or not
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register($prepend = false)
|
||||
{
|
||||
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
|
||||
|
||||
if (null === $this->vendorDir) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($prepend) {
|
||||
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
|
||||
} else {
|
||||
unset(self::$registeredLoaders[$this->vendorDir]);
|
||||
self::$registeredLoaders[$this->vendorDir] = $this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters this instance as an autoloader.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function unregister()
|
||||
{
|
||||
spl_autoload_unregister(array($this, 'loadClass'));
|
||||
|
||||
if (null !== $this->vendorDir) {
|
||||
unset(self::$registeredLoaders[$this->vendorDir]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the given class or interface.
|
||||
*
|
||||
* @param string $class The name of the class
|
||||
* @return true|null True if loaded, null otherwise
|
||||
*/
|
||||
public function loadClass($class)
|
||||
{
|
||||
if ($file = $this->findFile($class)) {
|
||||
$includeFile = self::$includeFile;
|
||||
$includeFile($file);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the path to the file where the class is defined.
|
||||
*
|
||||
* @param string $class The name of the class
|
||||
*
|
||||
* @return string|false The path if found, false otherwise
|
||||
*/
|
||||
public function findFile($class)
|
||||
{
|
||||
// class map lookup
|
||||
if (isset($this->classMap[$class])) {
|
||||
return $this->classMap[$class];
|
||||
}
|
||||
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
|
||||
return false;
|
||||
}
|
||||
if (null !== $this->apcuPrefix) {
|
||||
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
|
||||
if ($hit) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
|
||||
$file = $this->findFileWithExtension($class, '.php');
|
||||
|
||||
// Search for Hack files if we are running on HHVM
|
||||
if (false === $file && defined('HHVM_VERSION')) {
|
||||
$file = $this->findFileWithExtension($class, '.hh');
|
||||
}
|
||||
|
||||
if (null !== $this->apcuPrefix) {
|
||||
apcu_add($this->apcuPrefix.$class, $file);
|
||||
}
|
||||
|
||||
if (false === $file) {
|
||||
// Remember that this class does not exist.
|
||||
$this->missingClasses[$class] = true;
|
||||
}
|
||||
|
||||
return $file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currently registered loaders keyed by their corresponding vendor directories.
|
||||
*
|
||||
* @return array<string, self>
|
||||
*/
|
||||
public static function getRegisteredLoaders()
|
||||
{
|
||||
return self::$registeredLoaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $class
|
||||
* @param string $ext
|
||||
* @return string|false
|
||||
*/
|
||||
private function findFileWithExtension($class, $ext)
|
||||
{
|
||||
// PSR-4 lookup
|
||||
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
|
||||
|
||||
$first = $class[0];
|
||||
if (isset($this->prefixLengthsPsr4[$first])) {
|
||||
$subPath = $class;
|
||||
while (false !== $lastPos = strrpos($subPath, '\\')) {
|
||||
$subPath = substr($subPath, 0, $lastPos);
|
||||
$search = $subPath . '\\';
|
||||
if (isset($this->prefixDirsPsr4[$search])) {
|
||||
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
|
||||
foreach ($this->prefixDirsPsr4[$search] as $dir) {
|
||||
if (file_exists($file = $dir . $pathEnd)) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PSR-4 fallback dirs
|
||||
foreach ($this->fallbackDirsPsr4 as $dir) {
|
||||
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
|
||||
// PSR-0 lookup
|
||||
if (false !== $pos = strrpos($class, '\\')) {
|
||||
// namespaced class name
|
||||
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
|
||||
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
|
||||
} else {
|
||||
// PEAR-like class name
|
||||
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
|
||||
}
|
||||
|
||||
if (isset($this->prefixesPsr0[$first])) {
|
||||
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
|
||||
if (0 === strpos($class, $prefix)) {
|
||||
foreach ($dirs as $dir) {
|
||||
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PSR-0 fallback dirs
|
||||
foreach ($this->fallbackDirsPsr0 as $dir) {
|
||||
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
|
||||
// PSR-0 include paths.
|
||||
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
|
||||
return $file;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
private static function initializeIncludeClosure()
|
||||
{
|
||||
if (self::$includeFile !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope isolated include.
|
||||
*
|
||||
* Prevents access to $this/self from included files.
|
||||
*
|
||||
* @param string $file
|
||||
* @return void
|
||||
*/
|
||||
self::$includeFile = \Closure::bind(static function($file) {
|
||||
include $file;
|
||||
}, null, null);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,359 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Composer.
|
||||
*
|
||||
* (c) Nils Adermann <naderman@naderman.de>
|
||||
* Jordi Boggiano <j.boggiano@seld.be>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Composer;
|
||||
|
||||
use Composer\Autoload\ClassLoader;
|
||||
use Composer\Semver\VersionParser;
|
||||
|
||||
/**
|
||||
* This class is copied in every Composer installed project and available to all
|
||||
*
|
||||
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
|
||||
*
|
||||
* To require its presence, you can require `composer-runtime-api ^2.0`
|
||||
*
|
||||
* @final
|
||||
*/
|
||||
class InstalledVersions
|
||||
{
|
||||
/**
|
||||
* @var mixed[]|null
|
||||
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
|
||||
*/
|
||||
private static $installed;
|
||||
|
||||
/**
|
||||
* @var bool|null
|
||||
*/
|
||||
private static $canGetVendors;
|
||||
|
||||
/**
|
||||
* @var array[]
|
||||
* @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
|
||||
*/
|
||||
private static $installedByVendor = array();
|
||||
|
||||
/**
|
||||
* Returns a list of all package names which are present, either by being installed, replaced or provided
|
||||
*
|
||||
* @return string[]
|
||||
* @psalm-return list<string>
|
||||
*/
|
||||
public static function getInstalledPackages()
|
||||
{
|
||||
$packages = array();
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
$packages[] = array_keys($installed['versions']);
|
||||
}
|
||||
|
||||
if (1 === \count($packages)) {
|
||||
return $packages[0];
|
||||
}
|
||||
|
||||
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all package names with a specific type e.g. 'library'
|
||||
*
|
||||
* @param string $type
|
||||
* @return string[]
|
||||
* @psalm-return list<string>
|
||||
*/
|
||||
public static function getInstalledPackagesByType($type)
|
||||
{
|
||||
$packagesByType = array();
|
||||
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
foreach ($installed['versions'] as $name => $package) {
|
||||
if (isset($package['type']) && $package['type'] === $type) {
|
||||
$packagesByType[] = $name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $packagesByType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given package is installed
|
||||
*
|
||||
* This also returns true if the package name is provided or replaced by another package
|
||||
*
|
||||
* @param string $packageName
|
||||
* @param bool $includeDevRequirements
|
||||
* @return bool
|
||||
*/
|
||||
public static function isInstalled($packageName, $includeDevRequirements = true)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (isset($installed['versions'][$packageName])) {
|
||||
return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given package satisfies a version constraint
|
||||
*
|
||||
* e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
|
||||
*
|
||||
* Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
|
||||
*
|
||||
* @param VersionParser $parser Install composer/semver to have access to this class and functionality
|
||||
* @param string $packageName
|
||||
* @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
|
||||
* @return bool
|
||||
*/
|
||||
public static function satisfies(VersionParser $parser, $packageName, $constraint)
|
||||
{
|
||||
$constraint = $parser->parseConstraints((string) $constraint);
|
||||
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
|
||||
|
||||
return $provided->matches($constraint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a version constraint representing all the range(s) which are installed for a given package
|
||||
*
|
||||
* It is easier to use this via isInstalled() with the $constraint argument if you need to check
|
||||
* whether a given version of a package is installed, and not just whether it exists
|
||||
*
|
||||
* @param string $packageName
|
||||
* @return string Version constraint usable with composer/semver
|
||||
*/
|
||||
public static function getVersionRanges($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$ranges = array();
|
||||
if (isset($installed['versions'][$packageName]['pretty_version'])) {
|
||||
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
|
||||
}
|
||||
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
|
||||
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
|
||||
}
|
||||
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
|
||||
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
|
||||
}
|
||||
if (array_key_exists('provided', $installed['versions'][$packageName])) {
|
||||
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
|
||||
}
|
||||
|
||||
return implode(' || ', $ranges);
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
|
||||
*/
|
||||
public static function getVersion($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($installed['versions'][$packageName]['version'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $installed['versions'][$packageName]['version'];
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
|
||||
*/
|
||||
public static function getPrettyVersion($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $installed['versions'][$packageName]['pretty_version'];
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
|
||||
*/
|
||||
public static function getReference($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($installed['versions'][$packageName]['reference'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $installed['versions'][$packageName]['reference'];
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
|
||||
*/
|
||||
public static function getInstallPath($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
* @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
|
||||
*/
|
||||
public static function getRootPackage()
|
||||
{
|
||||
$installed = self::getInstalled();
|
||||
|
||||
return $installed[0]['root'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw installed.php data for custom implementations
|
||||
*
|
||||
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
|
||||
* @return array[]
|
||||
* @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
|
||||
*/
|
||||
public static function getRawData()
|
||||
{
|
||||
@trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
|
||||
|
||||
if (null === self::$installed) {
|
||||
// only require the installed.php file if this file is loaded from its dumped location,
|
||||
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
|
||||
if (substr(__DIR__, -8, 1) !== 'C') {
|
||||
self::$installed = include __DIR__ . '/installed.php';
|
||||
} else {
|
||||
self::$installed = array();
|
||||
}
|
||||
}
|
||||
|
||||
return self::$installed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw data of all installed.php which are currently loaded for custom implementations
|
||||
*
|
||||
* @return array[]
|
||||
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
|
||||
*/
|
||||
public static function getAllRawData()
|
||||
{
|
||||
return self::getInstalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lets you reload the static array from another file
|
||||
*
|
||||
* This is only useful for complex integrations in which a project needs to use
|
||||
* this class but then also needs to execute another project's autoloader in process,
|
||||
* and wants to ensure both projects have access to their version of installed.php.
|
||||
*
|
||||
* A typical case would be PHPUnit, where it would need to make sure it reads all
|
||||
* the data it needs from this class, then call reload() with
|
||||
* `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
|
||||
* the project in which it runs can then also use this class safely, without
|
||||
* interference between PHPUnit's dependencies and the project's dependencies.
|
||||
*
|
||||
* @param array[] $data A vendor/composer/installed.php data set
|
||||
* @return void
|
||||
*
|
||||
* @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
|
||||
*/
|
||||
public static function reload($data)
|
||||
{
|
||||
self::$installed = $data;
|
||||
self::$installedByVendor = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array[]
|
||||
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
|
||||
*/
|
||||
private static function getInstalled()
|
||||
{
|
||||
if (null === self::$canGetVendors) {
|
||||
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
|
||||
}
|
||||
|
||||
$installed = array();
|
||||
|
||||
if (self::$canGetVendors) {
|
||||
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
|
||||
if (isset(self::$installedByVendor[$vendorDir])) {
|
||||
$installed[] = self::$installedByVendor[$vendorDir];
|
||||
} elseif (is_file($vendorDir.'/composer/installed.php')) {
|
||||
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
|
||||
$required = require $vendorDir.'/composer/installed.php';
|
||||
$installed[] = self::$installedByVendor[$vendorDir] = $required;
|
||||
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
|
||||
self::$installed = $installed[count($installed) - 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (null === self::$installed) {
|
||||
// only require the installed.php file if this file is loaded from its dumped location,
|
||||
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
|
||||
if (substr(__DIR__, -8, 1) !== 'C') {
|
||||
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
|
||||
$required = require __DIR__ . '/installed.php';
|
||||
self::$installed = $required;
|
||||
} else {
|
||||
self::$installed = array();
|
||||
}
|
||||
}
|
||||
|
||||
if (self::$installed !== array()) {
|
||||
$installed[] = self::$installed;
|
||||
}
|
||||
|
||||
return $installed;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
|
||||
Copyright (c) Nils Adermann, Jordi Boggiano
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
|
||||
// autoload_classmap.php @generated by Composer
|
||||
|
||||
$vendorDir = dirname(__DIR__);
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
|
||||
'Dompdf\\Cpdf' => $vendorDir . '/dompdf/dompdf/lib/Cpdf.php',
|
||||
'Normalizer' => $vendorDir . '/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php',
|
||||
'ReturnTypeWillChange' => $vendorDir . '/symfony/polyfill-php81/Resources/stubs/ReturnTypeWillChange.php',
|
||||
'lessc' => $vendorDir . '/wikimedia/less.php/lessc.inc.php',
|
||||
);
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
// autoload_files.php @generated by Composer
|
||||
|
||||
$vendorDir = dirname(__DIR__);
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
'ad155f8f1cf0d418fe49e248db8c661b' => $vendorDir . '/react/promise/src/functions_include.php',
|
||||
'ebf8799635f67b5d7248946fe2154f4a' => $vendorDir . '/ringcentral/psr7/src/functions_include.php',
|
||||
'972fda704d680a3a53c68e34e193cb22' => $vendorDir . '/react/promise-timer/src/functions_include.php',
|
||||
'7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php',
|
||||
'cea474b4340aa9fa53661e887a21a316' => $vendorDir . '/react/promise-stream/src/functions_include.php',
|
||||
'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php',
|
||||
'25072dd6e2470089de65ae7bf11d3109' => $vendorDir . '/symfony/polyfill-php72/bootstrap.php',
|
||||
'c964ee0ededf28c96ebd9db5099ef910' => $vendorDir . '/guzzlehttp/promises/src/functions_include.php',
|
||||
'a0edc8309cc5e1d60e3047b5df6b7052' => $vendorDir . '/guzzlehttp/psr7/src/functions_include.php',
|
||||
'45b89995831374eefdfc4161161938f6' => $vendorDir . '/jfcherng/php-color-output/src/helpers.php',
|
||||
'320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php',
|
||||
'f598d06aa772fa33d905e87be6398fb1' => $vendorDir . '/symfony/polyfill-intl-idn/bootstrap.php',
|
||||
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
|
||||
'23c18046f52bef3eea034657bafda50f' => $vendorDir . '/symfony/polyfill-php81/bootstrap.php',
|
||||
'f67ee1ef46ff5893b59dcf4c4e98a0e8' => $vendorDir . '/clue/block-react/src/functions_include.php',
|
||||
'2cffec82183ee1cea088009cef9a6fc3' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier.composer.php',
|
||||
'37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',
|
||||
'e39a8b23c42d4e1452234d762b03835a' => $vendorDir . '/ramsey/uuid/src/functions.php',
|
||||
);
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
// autoload_namespaces.php @generated by Composer
|
||||
|
||||
$vendorDir = dirname(__DIR__);
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
'Zend_' => array($vendorDir . '/shardj/zf1-future/library'),
|
||||
'Parsedown' => array($vendorDir . '/erusev/parsedown'),
|
||||
'Less' => array($vendorDir . '/wikimedia/less.php/lib'),
|
||||
'JShrink' => array($vendorDir . '/tedivm/jshrink/src'),
|
||||
'HTMLPurifier' => array($vendorDir . '/ezyang/htmlpurifier/library'),
|
||||
'Evenement' => array($vendorDir . '/evenement/evenement/src'),
|
||||
'Clue\\Redis\\Protocol' => array($vendorDir . '/clue/redis-protocol/src'),
|
||||
'AssetLoader' => array($baseDir . '/'),
|
||||
);
|
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
|
||||
// autoload_psr4.php @generated by Composer
|
||||
|
||||
$vendorDir = dirname(__DIR__);
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
'Symfony\\Polyfill\\Php81\\' => array($vendorDir . '/symfony/polyfill-php81'),
|
||||
'Symfony\\Polyfill\\Php72\\' => array($vendorDir . '/symfony/polyfill-php72'),
|
||||
'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'),
|
||||
'Symfony\\Polyfill\\Intl\\Normalizer\\' => array($vendorDir . '/symfony/polyfill-intl-normalizer'),
|
||||
'Symfony\\Polyfill\\Intl\\Idn\\' => array($vendorDir . '/symfony/polyfill-intl-idn'),
|
||||
'Symfony\\Polyfill\\Ctype\\' => array($vendorDir . '/symfony/polyfill-ctype'),
|
||||
'Svg\\' => array($vendorDir . '/phenx/php-svg-lib/src/Svg'),
|
||||
'Socket\\Raw\\' => array($vendorDir . '/clue/socket-raw/src'),
|
||||
'Sabberworm\\CSS\\' => array($vendorDir . '/sabberworm/php-css-parser/src'),
|
||||
'RingCentral\\Psr7\\' => array($vendorDir . '/ringcentral/psr7/src'),
|
||||
'React\\Stream\\' => array($vendorDir . '/react/stream/src'),
|
||||
'React\\Socket\\' => array($vendorDir . '/react/socket/src'),
|
||||
'React\\Promise\\Timer\\' => array($vendorDir . '/react/promise-timer/src'),
|
||||
'React\\Promise\\Stream\\' => array($vendorDir . '/react/promise-stream/src'),
|
||||
'React\\Promise\\' => array($vendorDir . '/react/promise/src'),
|
||||
'React\\Http\\' => array($vendorDir . '/react/http/src'),
|
||||
'React\\HttpClient\\' => array($vendorDir . '/react/http-client/src'),
|
||||
'React\\EventLoop\\' => array($vendorDir . '/react/event-loop/src'),
|
||||
'React\\Dns\\' => array($vendorDir . '/react/dns/src'),
|
||||
'React\\Datagram\\' => array($vendorDir . '/react/datagram/src'),
|
||||
'React\\ChildProcess\\' => array($vendorDir . '/react/child-process/src'),
|
||||
'React\\Cache\\' => array($vendorDir . '/react/cache/src'),
|
||||
'Ramsey\\Uuid\\' => array($vendorDir . '/ramsey/uuid/src'),
|
||||
'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-message/src'),
|
||||
'Predis\\' => array($vendorDir . '/predis/predis/src'),
|
||||
'Masterminds\\' => array($vendorDir . '/masterminds/html5/src'),
|
||||
'Jfcherng\\Utility\\' => array($vendorDir . '/jfcherng/php-mb-string/src', $vendorDir . '/jfcherng/php-color-output/src'),
|
||||
'Jfcherng\\Diff\\' => array($vendorDir . '/jfcherng/php-sequence-matcher/src', $vendorDir . '/jfcherng/php-diff/src'),
|
||||
'GuzzleHttp\\Psr7\\' => array($vendorDir . '/guzzlehttp/psr7/src'),
|
||||
'GuzzleHttp\\Promise\\' => array($vendorDir . '/guzzlehttp/promises/src'),
|
||||
'GuzzleHttp\\' => array($vendorDir . '/guzzlehttp/guzzle/src'),
|
||||
'FontLib\\' => array($vendorDir . '/phenx/php-font-lib/src/FontLib'),
|
||||
'Fig\\Http\\Message\\' => array($vendorDir . '/fig/http-message-util/src'),
|
||||
'Dompdf\\' => array($vendorDir . '/dompdf/dompdf/src'),
|
||||
'ConnectionManager\\Extra\\' => array($vendorDir . '/clue/connection-manager-extra/src'),
|
||||
'Clue\\React\\Utf8\\' => array($vendorDir . '/clue/utf8-react/src'),
|
||||
'Clue\\React\\Term\\' => array($vendorDir . '/clue/term-react/src'),
|
||||
'Clue\\React\\Stdio\\' => array($vendorDir . '/clue/stdio-react/src'),
|
||||
'Clue\\React\\Socks\\' => array($vendorDir . '/clue/socks-react/src'),
|
||||
'Clue\\React\\Soap\\' => array($vendorDir . '/clue/soap-react/src'),
|
||||
'Clue\\React\\Redis\\' => array($vendorDir . '/clue/redis-react/src'),
|
||||
'Clue\\React\\Mq\\' => array($vendorDir . '/clue/mq-react/src'),
|
||||
'Clue\\React\\HttpProxy\\' => array($vendorDir . '/clue/http-proxy-react/src'),
|
||||
'Clue\\React\\Buzz\\' => array($vendorDir . '/clue/buzz-react/src'),
|
||||
);
|
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
|
||||
// autoload_real.php @generated by Composer
|
||||
|
||||
class ComposerAutoloaderInit90058a67762ccdd1948ae1961aa43e94
|
||||
{
|
||||
private static $loader;
|
||||
|
||||
public static function loadClassLoader($class)
|
||||
{
|
||||
if ('Composer\Autoload\ClassLoader' === $class) {
|
||||
require __DIR__ . '/ClassLoader.php';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Composer\Autoload\ClassLoader
|
||||
*/
|
||||
public static function getLoader()
|
||||
{
|
||||
if (null !== self::$loader) {
|
||||
return self::$loader;
|
||||
}
|
||||
|
||||
require __DIR__ . '/platform_check.php';
|
||||
|
||||
spl_autoload_register(array('ComposerAutoloaderInit90058a67762ccdd1948ae1961aa43e94', 'loadClassLoader'), true, true);
|
||||
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
|
||||
spl_autoload_unregister(array('ComposerAutoloaderInit90058a67762ccdd1948ae1961aa43e94', 'loadClassLoader'));
|
||||
|
||||
$includePaths = require __DIR__ . '/include_paths.php';
|
||||
$includePaths[] = get_include_path();
|
||||
set_include_path(implode(PATH_SEPARATOR, $includePaths));
|
||||
|
||||
require __DIR__ . '/autoload_static.php';
|
||||
call_user_func(\Composer\Autoload\ComposerStaticInit90058a67762ccdd1948ae1961aa43e94::getInitializer($loader));
|
||||
|
||||
$loader->register(true);
|
||||
|
||||
$filesToLoad = \Composer\Autoload\ComposerStaticInit90058a67762ccdd1948ae1961aa43e94::$files;
|
||||
$requireFile = \Closure::bind(static function ($fileIdentifier, $file) {
|
||||
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
|
||||
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
|
||||
|
||||
require $file;
|
||||
}
|
||||
}, null, null);
|
||||
foreach ($filesToLoad as $fileIdentifier => $file) {
|
||||
$requireFile($fileIdentifier, $file);
|
||||
}
|
||||
|
||||
return $loader;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,362 @@
|
|||
<?php
|
||||
|
||||
// autoload_static.php @generated by Composer
|
||||
|
||||
namespace Composer\Autoload;
|
||||
|
||||
class ComposerStaticInit90058a67762ccdd1948ae1961aa43e94
|
||||
{
|
||||
public static $files = array (
|
||||
'ad155f8f1cf0d418fe49e248db8c661b' => __DIR__ . '/..' . '/react/promise/src/functions_include.php',
|
||||
'ebf8799635f67b5d7248946fe2154f4a' => __DIR__ . '/..' . '/ringcentral/psr7/src/functions_include.php',
|
||||
'972fda704d680a3a53c68e34e193cb22' => __DIR__ . '/..' . '/react/promise-timer/src/functions_include.php',
|
||||
'7b11c4dc42b3b3023073cb14e519683c' => __DIR__ . '/..' . '/ralouphie/getallheaders/src/getallheaders.php',
|
||||
'cea474b4340aa9fa53661e887a21a316' => __DIR__ . '/..' . '/react/promise-stream/src/functions_include.php',
|
||||
'e69f7f6ee287b969198c3c9d6777bd38' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/bootstrap.php',
|
||||
'25072dd6e2470089de65ae7bf11d3109' => __DIR__ . '/..' . '/symfony/polyfill-php72/bootstrap.php',
|
||||
'c964ee0ededf28c96ebd9db5099ef910' => __DIR__ . '/..' . '/guzzlehttp/promises/src/functions_include.php',
|
||||
'a0edc8309cc5e1d60e3047b5df6b7052' => __DIR__ . '/..' . '/guzzlehttp/psr7/src/functions_include.php',
|
||||
'45b89995831374eefdfc4161161938f6' => __DIR__ . '/..' . '/jfcherng/php-color-output/src/helpers.php',
|
||||
'320cde22f66dd4f5d3fd621d3e88b98f' => __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php',
|
||||
'f598d06aa772fa33d905e87be6398fb1' => __DIR__ . '/..' . '/symfony/polyfill-intl-idn/bootstrap.php',
|
||||
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
|
||||
'23c18046f52bef3eea034657bafda50f' => __DIR__ . '/..' . '/symfony/polyfill-php81/bootstrap.php',
|
||||
'f67ee1ef46ff5893b59dcf4c4e98a0e8' => __DIR__ . '/..' . '/clue/block-react/src/functions_include.php',
|
||||
'2cffec82183ee1cea088009cef9a6fc3' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier.composer.php',
|
||||
'37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php',
|
||||
'e39a8b23c42d4e1452234d762b03835a' => __DIR__ . '/..' . '/ramsey/uuid/src/functions.php',
|
||||
);
|
||||
|
||||
public static $prefixLengthsPsr4 = array (
|
||||
'S' =>
|
||||
array (
|
||||
'Symfony\\Polyfill\\Php81\\' => 23,
|
||||
'Symfony\\Polyfill\\Php72\\' => 23,
|
||||
'Symfony\\Polyfill\\Mbstring\\' => 26,
|
||||
'Symfony\\Polyfill\\Intl\\Normalizer\\' => 33,
|
||||
'Symfony\\Polyfill\\Intl\\Idn\\' => 26,
|
||||
'Symfony\\Polyfill\\Ctype\\' => 23,
|
||||
'Svg\\' => 4,
|
||||
'Socket\\Raw\\' => 11,
|
||||
'Sabberworm\\CSS\\' => 15,
|
||||
),
|
||||
'R' =>
|
||||
array (
|
||||
'RingCentral\\Psr7\\' => 17,
|
||||
'React\\Stream\\' => 13,
|
||||
'React\\Socket\\' => 13,
|
||||
'React\\Promise\\Timer\\' => 20,
|
||||
'React\\Promise\\Stream\\' => 21,
|
||||
'React\\Promise\\' => 14,
|
||||
'React\\Http\\' => 11,
|
||||
'React\\HttpClient\\' => 17,
|
||||
'React\\EventLoop\\' => 16,
|
||||
'React\\Dns\\' => 10,
|
||||
'React\\Datagram\\' => 15,
|
||||
'React\\ChildProcess\\' => 19,
|
||||
'React\\Cache\\' => 12,
|
||||
'Ramsey\\Uuid\\' => 12,
|
||||
),
|
||||
'P' =>
|
||||
array (
|
||||
'Psr\\Http\\Message\\' => 17,
|
||||
'Predis\\' => 7,
|
||||
),
|
||||
'M' =>
|
||||
array (
|
||||
'Masterminds\\' => 12,
|
||||
),
|
||||
'J' =>
|
||||
array (
|
||||
'Jfcherng\\Utility\\' => 17,
|
||||
'Jfcherng\\Diff\\' => 14,
|
||||
),
|
||||
'G' =>
|
||||
array (
|
||||
'GuzzleHttp\\Psr7\\' => 16,
|
||||
'GuzzleHttp\\Promise\\' => 19,
|
||||
'GuzzleHttp\\' => 11,
|
||||
),
|
||||
'F' =>
|
||||
array (
|
||||
'FontLib\\' => 8,
|
||||
'Fig\\Http\\Message\\' => 17,
|
||||
),
|
||||
'D' =>
|
||||
array (
|
||||
'Dompdf\\' => 7,
|
||||
),
|
||||
'C' =>
|
||||
array (
|
||||
'ConnectionManager\\Extra\\' => 24,
|
||||
'Clue\\React\\Utf8\\' => 16,
|
||||
'Clue\\React\\Term\\' => 16,
|
||||
'Clue\\React\\Stdio\\' => 17,
|
||||
'Clue\\React\\Socks\\' => 17,
|
||||
'Clue\\React\\Soap\\' => 16,
|
||||
'Clue\\React\\Redis\\' => 17,
|
||||
'Clue\\React\\Mq\\' => 14,
|
||||
'Clue\\React\\HttpProxy\\' => 21,
|
||||
'Clue\\React\\Buzz\\' => 16,
|
||||
),
|
||||
);
|
||||
|
||||
public static $prefixDirsPsr4 = array (
|
||||
'Symfony\\Polyfill\\Php81\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/symfony/polyfill-php81',
|
||||
),
|
||||
'Symfony\\Polyfill\\Php72\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/symfony/polyfill-php72',
|
||||
),
|
||||
'Symfony\\Polyfill\\Mbstring\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring',
|
||||
),
|
||||
'Symfony\\Polyfill\\Intl\\Normalizer\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer',
|
||||
),
|
||||
'Symfony\\Polyfill\\Intl\\Idn\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/symfony/polyfill-intl-idn',
|
||||
),
|
||||
'Symfony\\Polyfill\\Ctype\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/symfony/polyfill-ctype',
|
||||
),
|
||||
'Svg\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/phenx/php-svg-lib/src/Svg',
|
||||
),
|
||||
'Socket\\Raw\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/clue/socket-raw/src',
|
||||
),
|
||||
'Sabberworm\\CSS\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/sabberworm/php-css-parser/src',
|
||||
),
|
||||
'RingCentral\\Psr7\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/ringcentral/psr7/src',
|
||||
),
|
||||
'React\\Stream\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/react/stream/src',
|
||||
),
|
||||
'React\\Socket\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/react/socket/src',
|
||||
),
|
||||
'React\\Promise\\Timer\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/react/promise-timer/src',
|
||||
),
|
||||
'React\\Promise\\Stream\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/react/promise-stream/src',
|
||||
),
|
||||
'React\\Promise\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/react/promise/src',
|
||||
),
|
||||
'React\\Http\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/react/http/src',
|
||||
),
|
||||
'React\\HttpClient\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/react/http-client/src',
|
||||
),
|
||||
'React\\EventLoop\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/react/event-loop/src',
|
||||
),
|
||||
'React\\Dns\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/react/dns/src',
|
||||
),
|
||||
'React\\Datagram\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/react/datagram/src',
|
||||
),
|
||||
'React\\ChildProcess\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/react/child-process/src',
|
||||
),
|
||||
'React\\Cache\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/react/cache/src',
|
||||
),
|
||||
'Ramsey\\Uuid\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/ramsey/uuid/src',
|
||||
),
|
||||
'Psr\\Http\\Message\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/psr/http-message/src',
|
||||
),
|
||||
'Predis\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/predis/predis/src',
|
||||
),
|
||||
'Masterminds\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/masterminds/html5/src',
|
||||
),
|
||||
'Jfcherng\\Utility\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/jfcherng/php-mb-string/src',
|
||||
1 => __DIR__ . '/..' . '/jfcherng/php-color-output/src',
|
||||
),
|
||||
'Jfcherng\\Diff\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/jfcherng/php-sequence-matcher/src',
|
||||
1 => __DIR__ . '/..' . '/jfcherng/php-diff/src',
|
||||
),
|
||||
'GuzzleHttp\\Psr7\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/guzzlehttp/psr7/src',
|
||||
),
|
||||
'GuzzleHttp\\Promise\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/guzzlehttp/promises/src',
|
||||
),
|
||||
'GuzzleHttp\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/guzzlehttp/guzzle/src',
|
||||
),
|
||||
'FontLib\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/phenx/php-font-lib/src/FontLib',
|
||||
),
|
||||
'Fig\\Http\\Message\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/fig/http-message-util/src',
|
||||
),
|
||||
'Dompdf\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/dompdf/dompdf/src',
|
||||
),
|
||||
'ConnectionManager\\Extra\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/clue/connection-manager-extra/src',
|
||||
),
|
||||
'Clue\\React\\Utf8\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/clue/utf8-react/src',
|
||||
),
|
||||
'Clue\\React\\Term\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/clue/term-react/src',
|
||||
),
|
||||
'Clue\\React\\Stdio\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/clue/stdio-react/src',
|
||||
),
|
||||
'Clue\\React\\Socks\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/clue/socks-react/src',
|
||||
),
|
||||
'Clue\\React\\Soap\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/clue/soap-react/src',
|
||||
),
|
||||
'Clue\\React\\Redis\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/clue/redis-react/src',
|
||||
),
|
||||
'Clue\\React\\Mq\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/clue/mq-react/src',
|
||||
),
|
||||
'Clue\\React\\HttpProxy\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/clue/http-proxy-react/src',
|
||||
),
|
||||
'Clue\\React\\Buzz\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/clue/buzz-react/src',
|
||||
),
|
||||
);
|
||||
|
||||
public static $prefixesPsr0 = array (
|
||||
'Z' =>
|
||||
array (
|
||||
'Zend_' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/shardj/zf1-future/library',
|
||||
),
|
||||
),
|
||||
'P' =>
|
||||
array (
|
||||
'Parsedown' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/erusev/parsedown',
|
||||
),
|
||||
),
|
||||
'L' =>
|
||||
array (
|
||||
'Less' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/wikimedia/less.php/lib',
|
||||
),
|
||||
),
|
||||
'J' =>
|
||||
array (
|
||||
'JShrink' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/tedivm/jshrink/src',
|
||||
),
|
||||
),
|
||||
'H' =>
|
||||
array (
|
||||
'HTMLPurifier' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/ezyang/htmlpurifier/library',
|
||||
),
|
||||
),
|
||||
'E' =>
|
||||
array (
|
||||
'Evenement' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/evenement/evenement/src',
|
||||
),
|
||||
),
|
||||
'C' =>
|
||||
array (
|
||||
'Clue\\Redis\\Protocol' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/clue/redis-protocol/src',
|
||||
),
|
||||
),
|
||||
'A' =>
|
||||
array (
|
||||
'AssetLoader' =>
|
||||
array (
|
||||
0 => __DIR__ . '/../..' . '/',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
public static $classMap = array (
|
||||
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
|
||||
'Dompdf\\Cpdf' => __DIR__ . '/..' . '/dompdf/dompdf/lib/Cpdf.php',
|
||||
'Normalizer' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php',
|
||||
'ReturnTypeWillChange' => __DIR__ . '/..' . '/symfony/polyfill-php81/Resources/stubs/ReturnTypeWillChange.php',
|
||||
'lessc' => __DIR__ . '/..' . '/wikimedia/less.php/lessc.inc.php',
|
||||
);
|
||||
|
||||
public static function getInitializer(ClassLoader $loader)
|
||||
{
|
||||
return \Closure::bind(function () use ($loader) {
|
||||
$loader->prefixLengthsPsr4 = ComposerStaticInit90058a67762ccdd1948ae1961aa43e94::$prefixLengthsPsr4;
|
||||
$loader->prefixDirsPsr4 = ComposerStaticInit90058a67762ccdd1948ae1961aa43e94::$prefixDirsPsr4;
|
||||
$loader->prefixesPsr0 = ComposerStaticInit90058a67762ccdd1948ae1961aa43e94::$prefixesPsr0;
|
||||
$loader->classMap = ComposerStaticInit90058a67762ccdd1948ae1961aa43e94::$classMap;
|
||||
|
||||
}, null, ClassLoader::class);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
// include_paths.php @generated by Composer
|
||||
|
||||
$vendorDir = dirname(__DIR__);
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
$vendorDir . '/shardj/zf1-future/library',
|
||||
);
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,554 @@
|
|||
<?php return array(
|
||||
'root' => array(
|
||||
'name' => 'icinga/icinga-php-thirdparty',
|
||||
'pretty_version' => 'dev-main',
|
||||
'version' => 'dev-main',
|
||||
'reference' => 'adb56cbe0a8d21a6d7d8bc705dd98f5ada668683',
|
||||
'type' => 'project',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
'dev' => true,
|
||||
),
|
||||
'versions' => array(
|
||||
'clue/block-react' => array(
|
||||
'pretty_version' => 'v1.5.0',
|
||||
'version' => '1.5.0.0',
|
||||
'reference' => '718b0571a94aa693c6fffc72182e87257ac900f3',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../clue/block-react',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'clue/buzz-react' => array(
|
||||
'pretty_version' => 'v2.9.0',
|
||||
'version' => '2.9.0.0',
|
||||
'reference' => 'eb180b5e0db00a3febaa458289706b8ca80ffe2a',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../clue/buzz-react',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'clue/connection-manager-extra' => array(
|
||||
'pretty_version' => 'v1.3.0',
|
||||
'version' => '1.3.0.0',
|
||||
'reference' => '6ddf7da4cd75bf5aa54be13eea7be9aad9e97ada',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../clue/connection-manager-extra',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'clue/http-proxy-react' => array(
|
||||
'pretty_version' => 'v1.8.0',
|
||||
'version' => '1.8.0.0',
|
||||
'reference' => '09366dd3e13b36b90f8e47a6acaf5a2c96b79fb5',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../clue/http-proxy-react',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'clue/mq-react' => array(
|
||||
'pretty_version' => 'v1.5.0',
|
||||
'version' => '1.5.0.0',
|
||||
'reference' => '6b57d63655760d6476b816ec481398afea52d677',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../clue/mq-react',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'clue/redis-protocol' => array(
|
||||
'pretty_version' => 'v0.3.1',
|
||||
'version' => '0.3.1.0',
|
||||
'reference' => '271b8009887209d930f613ad3b9518f94bd6b51c',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../clue/redis-protocol',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'clue/redis-react' => array(
|
||||
'pretty_version' => 'v2.6.0',
|
||||
'version' => '2.6.0.0',
|
||||
'reference' => 'f911455f9d7a77dd6f39c22548ddff521544b291',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../clue/redis-react',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'clue/soap-react' => array(
|
||||
'pretty_version' => 'v1.0.0',
|
||||
'version' => '1.0.0.0',
|
||||
'reference' => '72ec7d5a67ccfbfbc80cc349145c8b14f4a7f393',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../clue/soap-react',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'clue/socket-raw' => array(
|
||||
'pretty_version' => 'v1.6.0',
|
||||
'version' => '1.6.0.0',
|
||||
'reference' => '91e9f619f6769f931454a9882c21ffd7623d06cb',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../clue/socket-raw',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'clue/socks-react' => array(
|
||||
'pretty_version' => 'v1.4.0',
|
||||
'version' => '1.4.0.0',
|
||||
'reference' => 'eb3bd2ca5f43fea4b3bc4ef055179e0e6714730d',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../clue/socks-react',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'clue/stdio-react' => array(
|
||||
'pretty_version' => 'v2.6.0',
|
||||
'version' => '2.6.0.0',
|
||||
'reference' => 'dfa6c378aabdff718202d4e2453f752c38ea3399',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../clue/stdio-react',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'clue/term-react' => array(
|
||||
'pretty_version' => 'v1.3.0',
|
||||
'version' => '1.3.0.0',
|
||||
'reference' => 'eb6eb063eda04a714ef89f066586a2c49588f7ca',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../clue/term-react',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'clue/utf8-react' => array(
|
||||
'pretty_version' => 'v1.2.0',
|
||||
'version' => '1.2.0.0',
|
||||
'reference' => '8bc3f8c874cdf642c8f10f9ae93aadb8cd63da96',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../clue/utf8-react',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'components/jquery' => array(
|
||||
'pretty_version' => '3.6.0',
|
||||
'version' => '3.6.0.0',
|
||||
'reference' => '6cf38ee1fd04b6adf8e7dda161283aa35be818c3',
|
||||
'type' => 'component',
|
||||
'install_path' => __DIR__ . '/../components/jquery',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'dompdf/dompdf' => array(
|
||||
'pretty_version' => 'v2.0.3',
|
||||
'version' => '2.0.3.0',
|
||||
'reference' => 'e8d2d5e37e8b0b30f0732a011295ab80680d7e85',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../dompdf/dompdf',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'erusev/parsedown' => array(
|
||||
'pretty_version' => '1.7.4',
|
||||
'version' => '1.7.4.0',
|
||||
'reference' => 'cb17b6477dfff935958ba01325f2e8a2bfa6dab3',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../erusev/parsedown',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'evenement/evenement' => array(
|
||||
'pretty_version' => 'v3.0.1',
|
||||
'version' => '3.0.1.0',
|
||||
'reference' => '531bfb9d15f8aa57454f5f0285b18bec903b8fb7',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../evenement/evenement',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'ezyang/htmlpurifier' => array(
|
||||
'pretty_version' => 'v4.16.0',
|
||||
'version' => '4.16.0.0',
|
||||
'reference' => '523407fb06eb9e5f3d59889b3978d5bfe94299c8',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../ezyang/htmlpurifier',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'fig/http-message-util' => array(
|
||||
'pretty_version' => '1.1.5',
|
||||
'version' => '1.1.5.0',
|
||||
'reference' => '9d94dc0154230ac39e5bf89398b324a86f63f765',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../fig/http-message-util',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'guzzlehttp/guzzle' => array(
|
||||
'pretty_version' => '6.5.8',
|
||||
'version' => '6.5.8.0',
|
||||
'reference' => 'a52f0440530b54fa079ce76e8c5d196a42cad981',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../guzzlehttp/guzzle',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'guzzlehttp/promises' => array(
|
||||
'pretty_version' => '1.5.3',
|
||||
'version' => '1.5.3.0',
|
||||
'reference' => '67ab6e18aaa14d753cc148911d273f6e6cb6721e',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../guzzlehttp/promises',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'guzzlehttp/psr7' => array(
|
||||
'pretty_version' => '1.9.1',
|
||||
'version' => '1.9.1.0',
|
||||
'reference' => 'e4490cabc77465aaee90b20cfc9a770f8c04be6b',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../guzzlehttp/psr7',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'icinga/icinga-php-thirdparty' => array(
|
||||
'pretty_version' => 'dev-main',
|
||||
'version' => 'dev-main',
|
||||
'reference' => 'adb56cbe0a8d21a6d7d8bc705dd98f5ada668683',
|
||||
'type' => 'project',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'jfcherng/php-color-output' => array(
|
||||
'pretty_version' => '2.0.2',
|
||||
'version' => '2.0.2.0',
|
||||
'reference' => '2673074597eca9682d2fdfaee39a22418d4cc2f6',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../jfcherng/php-color-output',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'jfcherng/php-diff' => array(
|
||||
'pretty_version' => '6.10.14',
|
||||
'version' => '6.10.14.0',
|
||||
'reference' => '38b6e816badaa97f75541484d6c3d7c3e5dbca78',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../jfcherng/php-diff',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'jfcherng/php-mb-string' => array(
|
||||
'pretty_version' => '1.4.8',
|
||||
'version' => '1.4.8.0',
|
||||
'reference' => 'ef8926ff849863bfea234e99ee827947bedd86b0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../jfcherng/php-mb-string',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'jfcherng/php-sequence-matcher' => array(
|
||||
'pretty_version' => '3.2.10',
|
||||
'version' => '3.2.10.0',
|
||||
'reference' => '650164598be2c6ad6891dbd41de4cb5cc21cc91b',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../jfcherng/php-sequence-matcher',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'masterminds/html5' => array(
|
||||
'pretty_version' => '2.8.0',
|
||||
'version' => '2.8.0.0',
|
||||
'reference' => '3c5d5a56d56f48a1ca08a0670f0f80c1dad368f3',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../masterminds/html5',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'paragonie/random_compat' => array(
|
||||
'pretty_version' => 'v9.99.100',
|
||||
'version' => '9.99.100.0',
|
||||
'reference' => '996434e5492cb4c3edcb9168db6fbb1359ef965a',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../paragonie/random_compat',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'phenx/php-font-lib' => array(
|
||||
'pretty_version' => '0.5.4',
|
||||
'version' => '0.5.4.0',
|
||||
'reference' => 'dd448ad1ce34c63d09baccd05415e361300c35b4',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../phenx/php-font-lib',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'phenx/php-svg-lib' => array(
|
||||
'pretty_version' => '0.5.0',
|
||||
'version' => '0.5.0.0',
|
||||
'reference' => '76876c6cf3080bcb6f249d7d59705108166a6685',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../phenx/php-svg-lib',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'predis/predis' => array(
|
||||
'pretty_version' => 'v1.1.10',
|
||||
'version' => '1.1.10.0',
|
||||
'reference' => 'a2fb02d738bedadcffdbb07efa3a5e7bd57f8d6e',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../predis/predis',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'psr/http-message' => array(
|
||||
'pretty_version' => '1.1',
|
||||
'version' => '1.1.0.0',
|
||||
'reference' => 'cb6ce4845ce34a8ad9e68117c10ee90a29919eba',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../psr/http-message',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'psr/http-message-implementation' => array(
|
||||
'dev_requirement' => false,
|
||||
'provided' => array(
|
||||
0 => '1.0',
|
||||
),
|
||||
),
|
||||
'ralouphie/getallheaders' => array(
|
||||
'pretty_version' => '3.0.3',
|
||||
'version' => '3.0.3.0',
|
||||
'reference' => '120b605dfeb996808c31b6477290a714d356e822',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../ralouphie/getallheaders',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'ramsey/uuid' => array(
|
||||
'pretty_version' => '3.9.7',
|
||||
'version' => '3.9.7.0',
|
||||
'reference' => 'dc75aa439eb4c1b77f5379fd958b3dc0e6014178',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../ramsey/uuid',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'react/cache' => array(
|
||||
'pretty_version' => 'v1.2.0',
|
||||
'version' => '1.2.0.0',
|
||||
'reference' => 'd47c472b64aa5608225f47965a484b75c7817d5b',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../react/cache',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'react/child-process' => array(
|
||||
'pretty_version' => 'v0.6.5',
|
||||
'version' => '0.6.5.0',
|
||||
'reference' => 'e71eb1aa55f057c7a4a0d08d06b0b0a484bead43',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../react/child-process',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'react/datagram' => array(
|
||||
'pretty_version' => 'v1.9.0',
|
||||
'version' => '1.9.0.0',
|
||||
'reference' => '13c259bb2fd04f238eeb85f3783167eedec46e2c',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../react/datagram',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'react/dns' => array(
|
||||
'pretty_version' => 'v1.11.0',
|
||||
'version' => '1.11.0.0',
|
||||
'reference' => '3be0fc8f1eb37d6875cd6f0c6c7d0be81435de9f',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../react/dns',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'react/event-loop' => array(
|
||||
'pretty_version' => 'v1.4.0',
|
||||
'version' => '1.4.0.0',
|
||||
'reference' => '6e7e587714fff7a83dcc7025aee42ab3b265ae05',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../react/event-loop',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'react/http' => array(
|
||||
'pretty_version' => 'v1.9.0',
|
||||
'version' => '1.9.0.0',
|
||||
'reference' => 'bb3154dbaf2dfe3f0467f956a05f614a69d5f1d0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../react/http',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'react/http-client' => array(
|
||||
'pretty_version' => 'v0.5.11',
|
||||
'version' => '0.5.11.0',
|
||||
'reference' => '23dddb415b9bd36c81d1c78df63143e65702aa4b',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../react/http-client',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'react/promise' => array(
|
||||
'pretty_version' => 'v2.10.0',
|
||||
'version' => '2.10.0.0',
|
||||
'reference' => 'f913fb8cceba1e6644b7b90c4bfb678ed8a3ef38',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../react/promise',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'react/promise-stream' => array(
|
||||
'pretty_version' => 'v1.5.0',
|
||||
'version' => '1.5.0.0',
|
||||
'reference' => 'e6d2805e09ad50c4896f65f5e8705fe4ee7731a3',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../react/promise-stream',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'react/promise-timer' => array(
|
||||
'pretty_version' => 'v1.9.0',
|
||||
'version' => '1.9.0.0',
|
||||
'reference' => 'aa7a73c74b8d8c0f622f5982ff7b0351bc29e495',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../react/promise-timer',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'react/socket' => array(
|
||||
'pretty_version' => 'v1.13.0',
|
||||
'version' => '1.13.0.0',
|
||||
'reference' => 'cff482bbad5848ecbe8b57da57e4e213b03619aa',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../react/socket',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'react/stream' => array(
|
||||
'pretty_version' => 'v1.3.0',
|
||||
'version' => '1.3.0.0',
|
||||
'reference' => '6fbc9672905c7d5a885f2da2fc696f65840f4a66',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../react/stream',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'rhumsaa/uuid' => array(
|
||||
'dev_requirement' => false,
|
||||
'replaced' => array(
|
||||
0 => '3.9.7',
|
||||
),
|
||||
),
|
||||
'ringcentral/psr7' => array(
|
||||
'pretty_version' => '1.3.0',
|
||||
'version' => '1.3.0.0',
|
||||
'reference' => '360faaec4b563958b673fb52bbe94e37f14bc686',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../ringcentral/psr7',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'sabberworm/php-css-parser' => array(
|
||||
'pretty_version' => '8.4.0',
|
||||
'version' => '8.4.0.0',
|
||||
'reference' => 'e41d2140031d533348b2192a83f02d8dd8a71d30',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../sabberworm/php-css-parser',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'shardj/zf1-future' => array(
|
||||
'pretty_version' => '1.22.0',
|
||||
'version' => '1.22.0.0',
|
||||
'reference' => '5446af3466d1fc3faa32a708af2cdc9baf9fc11a',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../shardj/zf1-future',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'symfony/polyfill-ctype' => array(
|
||||
'pretty_version' => 'v1.27.0',
|
||||
'version' => '1.27.0.0',
|
||||
'reference' => '5bbc823adecdae860bb64756d639ecfec17b050a',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../symfony/polyfill-ctype',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'symfony/polyfill-intl-idn' => array(
|
||||
'pretty_version' => 'v1.27.0',
|
||||
'version' => '1.27.0.0',
|
||||
'reference' => '639084e360537a19f9ee352433b84ce831f3d2da',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../symfony/polyfill-intl-idn',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'symfony/polyfill-intl-normalizer' => array(
|
||||
'pretty_version' => 'v1.27.0',
|
||||
'version' => '1.27.0.0',
|
||||
'reference' => '19bd1e4fcd5b91116f14d8533c57831ed00571b6',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../symfony/polyfill-intl-normalizer',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'symfony/polyfill-mbstring' => array(
|
||||
'pretty_version' => 'v1.27.0',
|
||||
'version' => '1.27.0.0',
|
||||
'reference' => '8ad114f6b39e2c98a8b0e3bd907732c207c2b534',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../symfony/polyfill-mbstring',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'symfony/polyfill-php72' => array(
|
||||
'pretty_version' => 'v1.27.0',
|
||||
'version' => '1.27.0.0',
|
||||
'reference' => '869329b1e9894268a8a61dabb69153029b7a8c97',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../symfony/polyfill-php72',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'symfony/polyfill-php81' => array(
|
||||
'pretty_version' => 'v1.27.0',
|
||||
'version' => '1.27.0.0',
|
||||
'reference' => '707403074c8ea6e2edaf8794b0157a0bfa52157a',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../symfony/polyfill-php81',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'tedivm/jshrink' => array(
|
||||
'pretty_version' => 'v1.6.8',
|
||||
'version' => '1.6.8.0',
|
||||
'reference' => '35a83e0ab661d6130da5930c0c4eafcc663b8cec',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../tedivm/jshrink',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'wikimedia/less.php' => array(
|
||||
'pretty_version' => 'v3.2.1',
|
||||
'version' => '3.2.1.0',
|
||||
'reference' => '0d5b30ba792bdbf8991a646fc9c30561b38a5559',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../wikimedia/less.php',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'zendframework/zendframework1' => array(
|
||||
'dev_requirement' => false,
|
||||
'replaced' => array(
|
||||
0 => '>=1.12.20',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
// platform_check.php @generated by Composer
|
||||
|
||||
$issues = array();
|
||||
|
||||
if (!(PHP_VERSION_ID >= 70209)) {
|
||||
$issues[] = 'Your Composer dependencies require a PHP version ">= 7.2.9". You are running ' . PHP_VERSION . '.';
|
||||
}
|
||||
|
||||
if ($issues) {
|
||||
if (!headers_sent()) {
|
||||
header('HTTP/1.1 500 Internal Server Error');
|
||||
}
|
||||
if (!ini_get('display_errors')) {
|
||||
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
|
||||
fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
|
||||
} elseif (!headers_sent()) {
|
||||
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
|
||||
}
|
||||
}
|
||||
trigger_error(
|
||||
'Composer detected issues in your platform: ' . implode(' ', $issues),
|
||||
E_USER_ERROR
|
||||
);
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue