Version v0.1.4

This commit is contained in:
Thomas Gelf 2018-03-01 15:49:32 +01:00
parent 61c92c7e42
commit 56e8490e1a
999 changed files with 122402 additions and 0 deletions

7
vendor/autoload.php vendored Normal file
View File

@ -0,0 +1,7 @@
<?php
// autoload.php @generated by Composer
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit3214150501998a72ea353f9c1a1e903e::getLoader();

20
vendor/chrisboulton/php-resque/LICENSE vendored Normal file
View File

@ -0,0 +1,20 @@
(c) Chris Boulton <chris@bigcommerce.com>
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.

View File

@ -0,0 +1,9 @@
<?php
class Bad_PHP_Job
{
public function perform()
{
throw new Exception('Unable to run this job!');
}
}
?>

View File

@ -0,0 +1,21 @@
<?php
if(empty($argv[1])) {
die('Specify the ID of a job to monitor the status of.');
}
require '../lib/Resque/Job/Status.php';
require '../lib/Resque.php';
date_default_timezone_set('GMT');
Resque::setBackend('127.0.0.1:6379');
$status = new Resque_Job_Status($argv[1]);
if(!$status->isTracking()) {
die("Resque is not tracking the status of this job.\n");
}
echo "Tracking status of ".$argv[1].". Press [break] to stop.\n\n";
while(true) {
fwrite(STDOUT, "Status of ".$argv[1]." is: ".$status->get()."\n");
sleep(1);
}
?>

View File

@ -0,0 +1,10 @@
<?php
class PHP_Job
{
public function perform()
{
sleep(120);
fwrite(STDOUT, 'Hello!');
}
}
?>

View File

@ -0,0 +1,9 @@
<?php
class Long_PHP_Job
{
public function perform()
{
sleep(600);
}
}
?>

View File

@ -0,0 +1,9 @@
<?php
class PHP_Error_Job
{
public function perform()
{
callToUndefinedFunction();
}
}
?>

View File

@ -0,0 +1,19 @@
<?php
if(empty($argv[1])) {
die('Specify the name of a job to add. e.g, php queue.php PHP_Job');
}
require '../lib/Resque.php';
date_default_timezone_set('GMT');
Resque::setBackend('127.0.0.1:6379');
$args = array(
'time' => time(),
'array' => array(
'test' => 'test',
),
);
$jobId = Resque::enqueue('default', $argv[1], $args, true);
echo "Queued job ".$jobId."\n\n";
?>

View File

@ -0,0 +1,8 @@
<?php
date_default_timezone_set('GMT');
require 'bad_job.php';
require 'job.php';
require 'php_error_job.php';
require '../resque.php';
?>

View File

@ -0,0 +1,49 @@
<?php
// Somewhere in our application, we need to register:
Resque_Event::listen('afterEnqueue', array('My_Resque_Plugin', 'afterEnqueue'));
Resque_Event::listen('beforeFirstFork', array('My_Resque_Plugin', 'beforeFirstFork'));
Resque_Event::listen('beforeFork', array('My_Resque_Plugin', 'beforeFork'));
Resque_Event::listen('afterFork', array('My_Resque_Plugin', 'afterFork'));
Resque_Event::listen('beforePerform', array('My_Resque_Plugin', 'beforePerform'));
Resque_Event::listen('afterPerform', array('My_Resque_Plugin', 'afterPerform'));
Resque_Event::listen('onFailure', array('My_Resque_Plugin', 'onFailure'));
class My_Resque_Plugin
{
public static function afterEnqueue($class, $arguments)
{
echo "Job was queued for " . $class . ". Arguments:";
print_r($arguments);
}
public static function beforeFirstFork($worker)
{
echo "Worker started. Listening on queues: " . implode(', ', $worker->queues(false)) . "\n";
}
public static function beforeFork($job)
{
echo "Just about to fork to run " . $job;
}
public static function afterFork($job)
{
echo "Forked to run " . $job . ". This is the child process.\n";
}
public static function beforePerform($job)
{
echo "Cancelling " . $job . "\n";
// throw new Resque_Job_DontPerform;
}
public static function afterPerform($job)
{
echo "Just performed " . $job . "\n";
}
public static function onFailure($exception, $job)
{
echo $job . " threw an exception:\n" . $exception;
}
}

View File

@ -0,0 +1,22 @@
Copyright (c) 2009 Justin Poliey <jdp34@njit.edu>
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.

View File

@ -0,0 +1,150 @@
<?php
/**
* Redisent, a Redis interface for the modest
* @author Justin Poliey <jdp34@njit.edu>
* @copyright 2009 Justin Poliey <jdp34@njit.edu>
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
* @package Redisent
*/
define('CRLF', sprintf('%s%s', chr(13), chr(10)));
/**
* Wraps native Redis errors in friendlier PHP exceptions
* Only declared if class doesn't already exist to ensure compatibility with php-redis
*/
if (! class_exists('RedisException', false)) {
class RedisException extends Exception {
}
}
/**
* Redisent, a Redis interface for the modest among us
*/
class Redisent {
/**
* Socket connection to the Redis server
* @var resource
* @access private
*/
private $__sock;
/**
* Host of the Redis server
* @var string
* @access public
*/
public $host;
/**
* Port on which the Redis server is running
* @var integer
* @access public
*/
public $port;
/**
* Creates a Redisent connection to the Redis server on host {@link $host} and port {@link $port}.
* @param string $host The hostname of the Redis server
* @param integer $port The port number of the Redis server
*/
function __construct($host, $port = 6379) {
$this->host = $host;
$this->port = $port;
$this->establishConnection();
}
function establishConnection() {
$this->__sock = fsockopen($this->host, $this->port, $errno, $errstr);
if (!$this->__sock) {
throw new Exception("{$errno} - {$errstr}");
}
}
function __destruct() {
fclose($this->__sock);
}
function __call($name, $args) {
/* Build the Redis unified protocol command */
array_unshift($args, strtoupper($name));
$command = sprintf('*%d%s%s%s', count($args), CRLF, implode(array_map(array($this, 'formatArgument'), $args), CRLF), CRLF);
/* Open a Redis connection and execute the command */
for ($written = 0; $written < strlen($command); $written += $fwrite) {
$fwrite = fwrite($this->__sock, substr($command, $written));
if ($fwrite === FALSE) {
throw new Exception('Failed to write entire command to stream');
}
}
/* Parse the response based on the reply identifier */
$reply = trim(fgets($this->__sock, 512));
switch (substr($reply, 0, 1)) {
/* Error reply */
case '-':
throw new RedisException(substr(trim($reply), 4));
break;
/* Inline reply */
case '+':
$response = substr(trim($reply), 1);
break;
/* Bulk reply */
case '$':
$response = null;
if ($reply == '$-1') {
break;
}
$read = 0;
$size = substr($reply, 1);
do {
$block_size = ($size - $read) > 1024 ? 1024 : ($size - $read);
$response .= fread($this->__sock, $block_size);
$read += $block_size;
} while ($read < $size);
fread($this->__sock, 2); /* discard crlf */
break;
/* Multi-bulk reply */
case '*':
$count = substr($reply, 1);
if ($count == '-1') {
return null;
}
$response = array();
for ($i = 0; $i < $count; $i++) {
$bulk_head = trim(fgets($this->__sock, 512));
$size = substr($bulk_head, 1);
if ($size == '-1') {
$response[] = null;
}
else {
$read = 0;
$block = "";
do {
$block_size = ($size - $read) > 1024 ? 1024 : ($size - $read);
$block .= fread($this->__sock, $block_size);
$read += $block_size;
} while ($read < $size);
fread($this->__sock, 2); /* discard crlf */
$response[] = $block;
}
}
break;
/* Integer reply */
case ':':
$response = intval(substr(trim($reply), 1));
break;
default:
throw new RedisException("invalid server response: {$reply}");
break;
}
/* Party on */
return $response;
}
private function formatArgument($arg) {
return sprintf('$%d%s%s', strlen($arg), CRLF, $arg);
}
}

View File

@ -0,0 +1,138 @@
<?php
/**
* Redisent, a Redis interface for the modest
* @author Justin Poliey <jdp34@njit.edu>
* @copyright 2009 Justin Poliey <jdp34@njit.edu>
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
* @package Redisent
*/
require_once dirname(__FILE__) . '/Redisent.php';
/**
* A generalized Redisent interface for a cluster of Redis servers
*/
class RedisentCluster {
/**
* Collection of Redisent objects attached to Redis servers
* @var array
* @access private
*/
private $redisents;
/**
* Aliases of Redisent objects attached to Redis servers, used to route commands to specific servers
* @see RedisentCluster::to
* @var array
* @access private
*/
private $aliases;
/**
* Hash ring of Redis server nodes
* @var array
* @access private
*/
private $ring;
/**
* Individual nodes of pointers to Redis servers on the hash ring
* @var array
* @access private
*/
private $nodes;
/**
* Number of replicas of each node to make around the hash ring
* @var integer
* @access private
*/
private $replicas = 128;
/**
* The commands that are not subject to hashing
* @var array
* @access private
*/
private $dont_hash = array(
'RANDOMKEY', 'DBSIZE',
'SELECT', 'MOVE', 'FLUSHDB', 'FLUSHALL',
'SAVE', 'BGSAVE', 'LASTSAVE', 'SHUTDOWN',
'INFO', 'MONITOR', 'SLAVEOF'
);
/**
* Creates a Redisent interface to a cluster of Redis servers
* @param array $servers The Redis servers in the cluster. Each server should be in the format array('host' => hostname, 'port' => port)
*/
function __construct($servers) {
$this->ring = array();
$this->aliases = array();
foreach ($servers as $alias => $server) {
$this->redisents[] = new Redisent($server['host'], $server['port']);
if (is_string($alias)) {
$this->aliases[$alias] = $this->redisents[count($this->redisents)-1];
}
for ($replica = 1; $replica <= $this->replicas; $replica++) {
$this->ring[crc32($server['host'].':'.$server['port'].'-'.$replica)] = $this->redisents[count($this->redisents)-1];
}
}
ksort($this->ring, SORT_NUMERIC);
$this->nodes = array_keys($this->ring);
}
/**
* Routes a command to a specific Redis server aliased by {$alias}.
* @param string $alias The alias of the Redis server
* @return Redisent The Redisent object attached to the Redis server
*/
function to($alias) {
if (isset($this->aliases[$alias])) {
return $this->aliases[$alias];
}
else {
throw new Exception("That Redisent alias does not exist");
}
}
/* Execute a Redis command on the cluster */
function __call($name, $args) {
/* Pick a server node to send the command to */
$name = strtoupper($name);
if (!in_array($name, $this->dont_hash)) {
$node = $this->nextNode(crc32($args[0]));
$redisent = $this->ring[$node];
}
else {
$redisent = $this->redisents[0];
}
/* Execute the command on the server */
return call_user_func_array(array($redisent, $name), $args);
}
/**
* Routes to the proper server node
* @param integer $needle The hash value of the Redis command
* @return Redisent The Redisent object associated with the hash
*/
private function nextNode($needle) {
$haystack = $this->nodes;
while (count($haystack) > 2) {
$try = floor(count($haystack) / 2);
if ($haystack[$try] == $needle) {
return $needle;
}
if ($needle < $haystack[$try]) {
$haystack = array_slice($haystack, 0, $try + 1);
}
if ($needle > $haystack[$try]) {
$haystack = array_slice($haystack, $try + 1);
}
}
return $haystack[count($haystack)-1];
}
}

View File

@ -0,0 +1,189 @@
<?php
require_once dirname(__FILE__) . '/Resque/Event.php';
require_once dirname(__FILE__) . '/Resque/Exception.php';
/**
* Base Resque class.
*
* @package Resque
* @author Chris Boulton <chris@bigcommerce.com>
* @license http://www.opensource.org/licenses/mit-license.php
*/
class Resque
{
const VERSION = '1.2';
/**
* @var Resque_Redis Instance of Resque_Redis that talks to redis.
*/
public static $redis = null;
/**
* @var mixed Host/port conbination separated by a colon, or a nested
* array of server swith host/port pairs
*/
protected static $redisServer = null;
/**
* @var int ID of Redis database to select.
*/
protected static $redisDatabase = 0;
/**
* @var int PID of current process. Used to detect changes when forking
* and implement "thread" safety to avoid race conditions.
*/
protected static $pid = null;
/**
* Given a host/port combination separated by a colon, set it as
* the redis server that Resque will talk to.
*
* @param mixed $server Host/port combination separated by a colon, or
* a nested array of servers with host/port pairs.
* @param int $database
*/
public static function setBackend($server, $database = 0)
{
self::$redisServer = $server;
self::$redisDatabase = $database;
self::$redis = null;
}
/**
* Return an instance of the Resque_Redis class instantiated for Resque.
*
* @return Resque_Redis Instance of Resque_Redis.
*/
public static function redis()
{
// Detect when the PID of the current process has changed (from a fork, etc)
// and force a reconnect to redis.
$pid = getmypid();
if (self::$pid !== $pid) {
self::$redis = null;
self::$pid = $pid;
}
if(!is_null(self::$redis)) {
return self::$redis;
}
$server = self::$redisServer;
if (empty($server)) {
$server = 'localhost:6379';
}
if(is_array($server)) {
require_once dirname(__FILE__) . '/Resque/RedisCluster.php';
self::$redis = new Resque_RedisCluster($server);
}
else {
if (strpos($server, 'unix:') === false) {
list($host, $port) = explode(':', $server);
}
else {
$host = $server;
$port = null;
}
require_once dirname(__FILE__) . '/Resque/Redis.php';
self::$redis = new Resque_Redis($host, $port);
}
self::$redis->select(self::$redisDatabase);
return self::$redis;
}
/**
* Push a job to the end of a specific queue. If the queue does not
* exist, then create it as well.
*
* @param string $queue The name of the queue to add the job to.
* @param array $item Job description as an array to be JSON encoded.
*/
public static function push($queue, $item)
{
self::redis()->sadd('queues', $queue);
self::redis()->rpush('queue:' . $queue, json_encode($item));
}
/**
* Pop an item off the end of the specified queue, decode it and
* return it.
*
* @param string $queue The name of the queue to fetch an item from.
* @return array Decoded item from the queue.
*/
public static function pop($queue)
{
$item = self::redis()->lpop('queue:' . $queue);
if(!$item) {
return;
}
return json_decode($item, true);
}
/**
* Return the size (number of pending jobs) of the specified queue.
*
* @param $queue name of the queue to be checked for pending jobs
*
* @return int The size of the queue.
*/
public static function size($queue)
{
return self::redis()->llen('queue:' . $queue);
}
/**
* Create a new job and save it to the specified queue.
*
* @param string $queue The name of the queue to place the job in.
* @param string $class The name of the class that contains the code to execute the job.
* @param array $args Any optional arguments that should be passed when the job is executed.
* @param boolean $trackStatus Set to true to be able to monitor the status of a job.
*
* @return string
*/
public static function enqueue($queue, $class, $args = null, $trackStatus = false)
{
require_once dirname(__FILE__) . '/Resque/Job.php';
$result = Resque_Job::create($queue, $class, $args, $trackStatus);
if ($result) {
Resque_Event::trigger('afterEnqueue', array(
'class' => $class,
'args' => $args,
'queue' => $queue,
));
}
return $result;
}
/**
* Reserve and return the next available job in the specified queue.
*
* @param string $queue Queue to fetch next available job from.
* @return Resque_Job Instance of Resque_Job to be processed, false if none or error.
*/
public static function reserve($queue)
{
require_once dirname(__FILE__) . '/Resque/Job.php';
return Resque_Job::reserve($queue);
}
/**
* Get an array of all known queues.
*
* @return array Array of queues.
*/
public static function queues()
{
$queues = self::redis()->smembers('queues');
if(!is_array($queues)) {
$queues = array();
}
return $queues;
}
}

View File

@ -0,0 +1,88 @@
<?php
/**
* Resque event/plugin system class
*
* @package Resque/Event
* @author Chris Boulton <chris@bigcommerce.com>
* @license http://www.opensource.org/licenses/mit-license.php
*/
class Resque_Event
{
/**
* @var array Array containing all registered callbacks, indexked by event name.
*/
private static $events = array();
/**
* Raise a given event with the supplied data.
*
* @param string $event Name of event to be raised.
* @param mixed $data Optional, any data that should be passed to each callback.
* @return true
*/
public static function trigger($event, $data = null)
{
if (!is_array($data)) {
$data = array($data);
}
if (empty(self::$events[$event])) {
return true;
}
foreach (self::$events[$event] as $callback) {
if (!is_callable($callback)) {
continue;
}
call_user_func_array($callback, $data);
}
return true;
}
/**
* Listen in on a given event to have a specified callback fired.
*
* @param string $event Name of event to listen on.
* @param mixed $callback Any callback callable by call_user_func_array.
* @return true
*/
public static function listen($event, $callback)
{
if (!isset(self::$events[$event])) {
self::$events[$event] = array();
}
self::$events[$event][] = $callback;
return true;
}
/**
* Stop a given callback from listening on a specific event.
*
* @param string $event Name of event.
* @param mixed $callback The callback as defined when listen() was called.
* @return true
*/
public static function stopListening($event, $callback)
{
if (!isset(self::$events[$event])) {
return true;
}
$key = array_search($callback, self::$events[$event]);
if ($key !== false) {
unset(self::$events[$event][$key]);
}
return true;
}
/**
* Call all registered listeners.
*/
public static function clearListeners()
{
self::$events = array();
}
}

View File

@ -0,0 +1,12 @@
<?php
/**
* Resque exception.
*
* @package Resque
* @author Chris Boulton <chris@bigcommerce.com>
* @license http://www.opensource.org/licenses/mit-license.php
*/
class Resque_Exception extends Exception
{
}
?>

View File

@ -0,0 +1,58 @@
<?php
require_once dirname(__FILE__) . '/Failure/Interface.php';
/**
* Failed Resque job.
*
* @package Resque/Failure
* @author Chris Boulton <chris@bigcommerce.com>
* @license http://www.opensource.org/licenses/mit-license.php
*/
class Resque_Failure
{
/**
* @var string Class name representing the backend to pass failed jobs off to.
*/
private static $backend;
/**
* Create a new failed job on the backend.
*
* @param object $payload The contents of the job that has just failed.
* @param \Exception $exception The exception generated when the job failed to run.
* @param \Resque_Worker $worker Instance of Resque_Worker that was running this job when it failed.
* @param string $queue The name of the queue that this job was fetched from.
*/
public static function create($payload, Exception $exception, Resque_Worker $worker, $queue)
{
$backend = self::getBackend();
new $backend($payload, $exception, $worker, $queue);
}
/**
* Return an instance of the backend for saving job failures.
*
* @return object Instance of backend object.
*/
public static function getBackend()
{
if(self::$backend === null) {
require dirname(__FILE__) . '/Failure/Redis.php';
self::$backend = 'Resque_Failure_Redis';
}
return self::$backend;
}
/**
* Set the backend to use for raised job failures. The supplied backend
* should be the name of a class to be instantiated when a job fails.
* It is your responsibility to have the backend class loaded (or autoloaded)
*
* @param string $backend The class name of the backend to pipe failures to.
*/
public static function setBackend($backend)
{
self::$backend = $backend;
}
}

View File

@ -0,0 +1,21 @@
<?php
/**
* Interface that all failure backends should implement.
*
* @package Resque/Failure
* @author Chris Boulton <chris@bigcommerce.com>
* @license http://www.opensource.org/licenses/mit-license.php
*/
interface Resque_Failure_Interface
{
/**
* Initialize a failed job class and save it (where appropriate).
*
* @param object $payload Object containing details of the failed job.
* @param object $exception Instance of the exception that was thrown by the failed job.
* @param object $worker Instance of Resque_Worker that received the job.
* @param string $queue The name of the queue the job was fetched from.
*/
public function __construct($payload, $exception, $worker, $queue);
}
?>

View File

@ -0,0 +1,34 @@
<?php
/**
* Redis backend for storing failed Resque jobs.
*
* @package Resque/Failure
* @author Chris Boulton <chris@bigcommerce.com>
* @license http://www.opensource.org/licenses/mit-license.php
*/
class Resque_Failure_Redis implements Resque_Failure_Interface
{
/**
* Initialize a failed job class and save it (where appropriate).
*
* @param object $payload Object containing details of the failed job.
* @param object $exception Instance of the exception that was thrown by the failed job.
* @param object $worker Instance of Resque_Worker that received the job.
* @param string $queue The name of the queue the job was fetched from.
*/
public function __construct($payload, $exception, $worker, $queue)
{
$data = new stdClass;
$data->failed_at = strftime('%a %b %d %H:%M:%S %Z %Y');
$data->payload = $payload;
$data->exception = get_class($exception);
$data->error = $exception->getMessage();
$data->backtrace = explode("\n", $exception->getTraceAsString());
$data->worker = (string)$worker;
$data->queue = $queue;
$data = json_encode($data);
Resque::redis()->rpush('failed', $data);
}
}
?>

View File

@ -0,0 +1,257 @@
<?php
require_once dirname(__FILE__) . '/Event.php';
require_once dirname(__FILE__) . '/Job/Status.php';
require_once dirname(__FILE__) . '/Job/DontPerform.php';
/**
* Resque job.
*
* @package Resque/Job
* @author Chris Boulton <chris@bigcommerce.com>
* @license http://www.opensource.org/licenses/mit-license.php
*/
class Resque_Job
{
/**
* @var string The name of the queue that this job belongs to.
*/
public $queue;
/**
* @var Resque_Worker Instance of the Resque worker running this job.
*/
public $worker;
/**
* @var object Object containing details of the job.
*/
public $payload;
/**
* @var object Instance of the class performing work for this job.
*/
private $instance;
/**
* Instantiate a new instance of a job.
*
* @param string $queue The queue that the job belongs to.
* @param array $payload array containing details of the job.
*/
public function __construct($queue, $payload)
{
$this->queue = $queue;
$this->payload = $payload;
}
/**
* Create a new job and save it to the specified queue.
*
* @param string $queue The name of the queue to place the job in.
* @param string $class The name of the class that contains the code to execute the job.
* @param array $args Any optional arguments that should be passed when the job is executed.
* @param boolean $monitor Set to true to be able to monitor the status of a job.
*
* @return string
*/
public static function create($queue, $class, $args = null, $monitor = false)
{
if($args !== null && !is_array($args)) {
throw new InvalidArgumentException(
'Supplied $args must be an array.'
);
}
$id = md5(uniqid('', true));
Resque::push($queue, array(
'class' => $class,
'args' => array($args),
'id' => $id,
));
if($monitor) {
Resque_Job_Status::create($id);
}
return $id;
}
/**
* Find the next available job from the specified queue and return an
* instance of Resque_Job for it.
*
* @param string $queue The name of the queue to check for a job in.
* @return null|object Null when there aren't any waiting jobs, instance of Resque_Job when a job was found.
*/
public static function reserve($queue)
{
$payload = Resque::pop($queue);
if(!is_array($payload)) {
return false;
}
return new Resque_Job($queue, $payload);
}
/**
* Update the status of the current job.
*
* @param int $status Status constant from Resque_Job_Status indicating the current status of a job.
*/
public function updateStatus($status)
{
if(empty($this->payload['id'])) {
return;
}
$statusInstance = new Resque_Job_Status($this->payload['id']);
$statusInstance->update($status);
}
/**
* Return the status of the current job.
*
* @return int The status of the job as one of the Resque_Job_Status constants.
*/
public function getStatus()
{
$status = new Resque_Job_Status($this->payload['id']);
return $status->get();
}
/**
* Get the arguments supplied to this job.
*
* @return array Array of arguments.
*/
public function getArguments()
{
if (!isset($this->payload['args'])) {
return array();
}
return $this->payload['args'][0];
}
/**
* Get the instantiated object for this job that will be performing work.
*
* @return object Instance of the object that this job belongs to.
*/
public function getInstance()
{
if (!is_null($this->instance)) {
return $this->instance;
}
if(!class_exists($this->payload['class'])) {
throw new Resque_Exception(
'Could not find job class ' . $this->payload['class'] . '.'
);
}
if(!method_exists($this->payload['class'], 'perform')) {
throw new Resque_Exception(
'Job class ' . $this->payload['class'] . ' does not contain a perform method.'
);
}
$this->instance = new $this->payload['class']();
$this->instance->job = $this;
$this->instance->args = $this->getArguments();
$this->instance->queue = $this->queue;
return $this->instance;
}
/**
* Actually execute a job by calling the perform method on the class
* associated with the job with the supplied arguments.
*
* @return bool
* @throws Resque_Exception When the job's class could not be found or it does not contain a perform method.
*/
public function perform()
{
$instance = $this->getInstance();
try {
Resque_Event::trigger('beforePerform', $this);
if(method_exists($instance, 'setUp')) {
$instance->setUp();
}
$instance->perform();
if(method_exists($instance, 'tearDown')) {
$instance->tearDown();
}
Resque_Event::trigger('afterPerform', $this);
}
// beforePerform/setUp have said don't perform this job. Return.
catch(Resque_Job_DontPerform $e) {
return false;
}
return true;
}
/**
* Mark the current job as having failed.
*
* @param $exception
*/
public function fail($exception)
{
Resque_Event::trigger('onFailure', array(
'exception' => $exception,
'job' => $this,
));
$this->updateStatus(Resque_Job_Status::STATUS_FAILED);
require_once dirname(__FILE__) . '/Failure.php';
Resque_Failure::create(
$this->payload,
$exception,
$this->worker,
$this->queue
);
Resque_Stat::incr('failed');
Resque_Stat::incr('failed:' . $this->worker);
}
/**
* Re-queue the current job.
* @return string
*/
public function recreate()
{
$status = new Resque_Job_Status($this->payload['id']);
$monitor = false;
if($status->isTracking()) {
$monitor = true;
}
return self::create($this->queue, $this->payload['class'], $this->payload['args'], $monitor);
}
/**
* Generate a string representation used to describe the current job.
*
* @return string The string representation of the job.
*/
public function __toString()
{
$name = array(
'Job{' . $this->queue .'}'
);
if(!empty($this->payload['id'])) {
$name[] = 'ID: ' . $this->payload['id'];
}
$name[] = $this->payload['class'];
if(!empty($this->payload['args'])) {
$name[] = json_encode($this->payload['args']);
}
return '(' . implode(' | ', $name) . ')';
}
}
?>

View File

@ -0,0 +1,12 @@
<?php
/**
* Runtime exception class for a job that does not exit cleanly.
*
* @package Resque/Job
* @author Chris Boulton <chris@bigcommerce.com>
* @license http://www.opensource.org/licenses/mit-license.php
*/
class Resque_Job_DirtyExitException extends RuntimeException
{
}

View File

@ -0,0 +1,12 @@
<?php
/**
* Exception to be thrown if a job should not be performed/run.
*
* @package Resque/Job
* @author Chris Boulton <chris@bigcommerce.com>
* @license http://www.opensource.org/licenses/mit-license.php
*/
class Resque_Job_DontPerform extends Exception
{
}

View File

@ -0,0 +1,143 @@
<?php
/**
* Status tracker/information for a job.
*
* @package Resque/Job
* @author Chris Boulton <chris@bigcommerce.com>
* @license http://www.opensource.org/licenses/mit-license.php
*/
class Resque_Job_Status
{
const STATUS_WAITING = 1;
const STATUS_RUNNING = 2;
const STATUS_FAILED = 3;
const STATUS_COMPLETE = 4;
/**
* @var string The ID of the job this status class refers back to.
*/
private $id;
/**
* @var mixed Cache variable if the status of this job is being monitored or not.
* True/false when checked at least once or null if not checked yet.
*/
private $isTracking = null;
/**
* @var array Array of statuses that are considered final/complete.
*/
private static $completeStatuses = array(
self::STATUS_FAILED,
self::STATUS_COMPLETE
);
/**
* Setup a new instance of the job monitor class for the supplied job ID.
*
* @param string $id The ID of the job to manage the status for.
*/
public function __construct($id)
{
$this->id = $id;
}
/**
* Create a new status monitor item for the supplied job ID. Will create
* all necessary keys in Redis to monitor the status of a job.
*
* @param string $id The ID of the job to monitor the status of.
*/
public static function create($id)
{
$statusPacket = array(
'status' => self::STATUS_WAITING,
'updated' => time(),
'started' => time(),
);
Resque::redis()->set('job:' . $id . ':status', json_encode($statusPacket));
}
/**
* Check if we're actually checking the status of the loaded job status
* instance.
*
* @return boolean True if the status is being monitored, false if not.
*/
public function isTracking()
{
if($this->isTracking === false) {
return false;
}
if(!Resque::redis()->exists((string)$this)) {
$this->isTracking = false;
return false;
}
$this->isTracking = true;
return true;
}
/**
* Update the status indicator for the current job with a new status.
*
* @param int The status of the job (see constants in Resque_Job_Status)
*/
public function update($status)
{
if(!$this->isTracking()) {
return;
}
$statusPacket = array(
'status' => $status,
'updated' => time(),
);
Resque::redis()->set((string)$this, json_encode($statusPacket));
// Expire the status for completed jobs after 24 hours
if(in_array($status, self::$completeStatuses)) {
Resque::redis()->expire((string)$this, 86400);
}
}
/**
* Fetch the status for the job being monitored.
*
* @return mixed False if the status is not being monitored, otherwise the status as
* as an integer, based on the Resque_Job_Status constants.
*/
public function get()
{
if(!$this->isTracking()) {
return false;
}
$statusPacket = json_decode(Resque::redis()->get((string)$this), true);
if(!$statusPacket) {
return false;
}
return $statusPacket['status'];
}
/**
* Stop tracking the status of a job.
*/
public function stop()
{
Resque::redis()->del((string)$this);
}
/**
* Generate a string representation of this object.
*
* @return string String representation of the current job status class.
*/
public function __toString()
{
return 'job:' . $this->id . ':status';
}
}
?>

View File

@ -0,0 +1,117 @@
<?php
// Third- party apps may have already loaded Resident from elsewhere
// so lets be careful.
if(!class_exists('Redisent', false)) {
require_once dirname(__FILE__) . '/../Redisent/Redisent.php';
}
/**
* Extended Redisent class used by Resque for all communication with
* redis. Essentially adds namespace support to Redisent.
*
* @package Resque/Redis
* @author Chris Boulton <chris@bigcommerce.com>
* @license http://www.opensource.org/licenses/mit-license.php
*/
class Resque_Redis extends Redisent
{
/**
* Redis namespace
* @var string
*/
private static $defaultNamespace = 'resque:';
/**
* @var array List of all commands in Redis that supply a key as their
* first argument. Used to prefix keys with the Resque namespace.
*/
private $keyCommands = array(
'exists',
'del',
'type',
'keys',
'expire',
'ttl',
'move',
'set',
'get',
'getset',
'setnx',
'incr',
'incrby',
'decr',
'decrby',
'rpush',
'lpush',
'llen',
'lrange',
'ltrim',
'lindex',
'lset',
'lrem',
'lpop',
'rpop',
'sadd',
'srem',
'spop',
'scard',
'sismember',
'smembers',
'srandmember',
'zadd',
'zrem',
'zrange',
'zrevrange',
'zrangebyscore',
'zcard',
'zscore',
'zremrangebyscore',
'sort'
);
// sinterstore
// sunion
// sunionstore
// sdiff
// sdiffstore
// sinter
// smove
// rename
// rpoplpush
// mget
// msetnx
// mset
// renamenx
/**
* Set Redis namespace (prefix) default: resque
* @param string $namespace
*/
public static function prefix($namespace)
{
if (strpos($namespace, ':') === false) {
$namespace .= ':';
}
self::$defaultNamespace = $namespace;
}
/**
* Magic method to handle all function requests and prefix key based
* operations with the {self::$defaultNamespace} key prefix.
*
* @param string $name The name of the method called.
* @param array $args Array of supplied arguments to the method.
* @return mixed Return value from Resident::call() based on the command.
*/
public function __call($name, $args) {
$args = func_get_args();
if(in_array($name, $this->keyCommands)) {
$args[1][0] = self::$defaultNamespace . $args[1][0];
}
try {
return parent::__call($name, $args[1]);
}
catch(RedisException $e) {
return false;
}
}
}
?>

View File

@ -0,0 +1,117 @@
<?php
// Third- party apps may have already loaded Resident from elsewhere
// so lets be careful.
if(!class_exists('RedisentCluster', false)) {
require_once dirname(__FILE__) . '/../Redisent/RedisentCluster.php';
}
/**
* Extended Redisent class used by Resque for all communication with
* redis. Essentially adds namespace support to Redisent.
*
* @package Resque/Redis
* @author Chris Boulton <chris@bigcommerce.com>
* @license http://www.opensource.org/licenses/mit-license.php
*/
class Resque_RedisCluster extends RedisentCluster
{
/**
* Redis namespace
* @var string
*/
private static $defaultNamespace = 'resque:';
/**
* @var array List of all commands in Redis that supply a key as their
* first argument. Used to prefix keys with the Resque namespace.
*/
private $keyCommands = array(
'exists',
'del',
'type',
'keys',
'expire',
'ttl',
'move',
'set',
'get',
'getset',
'setnx',
'incr',
'incrby',
'decrby',
'decrby',
'rpush',
'lpush',
'llen',
'lrange',
'ltrim',
'lindex',
'lset',
'lrem',
'lpop',
'rpop',
'sadd',
'srem',
'spop',
'scard',
'sismember',
'smembers',
'srandmember',
'zadd',
'zrem',
'zrange',
'zrevrange',
'zrangebyscore',
'zcard',
'zscore',
'zremrangebyscore',
'sort'
);
// sinterstore
// sunion
// sunionstore
// sdiff
// sdiffstore
// sinter
// smove
// rename
// rpoplpush
// mget
// msetnx
// mset
// renamenx
/**
* Set Redis namespace (prefix) default: resque
* @param string $namespace
*/
public static function prefix($namespace)
{
if (strpos($namespace, ':') === false) {
$namespace .= ':';
}
self::$defaultNamespace = $namespace;
}
/**
* Magic method to handle all function requests and prefix key based
* operations with the '{self::$defaultNamespace}' key prefix.
*
* @param string $name The name of the method called.
* @param array $args Array of supplied arguments to the method.
* @return mixed Return value from Resident::call() based on the command.
*/
public function __call($name, $args) {
$args = func_get_args();
if(in_array($name, $this->keyCommands)) {
$args[1][0] = self::$defaultNamespace . $args[1][0];
}
try {
return parent::__call($name, $args[1]);
}
catch(RedisException $e) {
return false;
}
}
}
?>

View File

@ -0,0 +1,56 @@
<?php
/**
* Resque statistic management (jobs processed, failed, etc)
*
* @package Resque/Stat
* @author Chris Boulton <chris@bigcommerce.com>
* @license http://www.opensource.org/licenses/mit-license.php
*/
class Resque_Stat
{
/**
* Get the value of the supplied statistic counter for the specified statistic.
*
* @param string $stat The name of the statistic to get the stats for.
* @return mixed Value of the statistic.
*/
public static function get($stat)
{
return (int)Resque::redis()->get('stat:' . $stat);
}
/**
* Increment the value of the specified statistic by a certain amount (default is 1)
*
* @param string $stat The name of the statistic to increment.
* @param int $by The amount to increment the statistic by.
* @return boolean True if successful, false if not.
*/
public static function incr($stat, $by = 1)
{
return (bool)Resque::redis()->incrby('stat:' . $stat, $by);
}
/**
* Decrement the value of the specified statistic by a certain amount (default is 1)
*
* @param string $stat The name of the statistic to decrement.
* @param int $by The amount to decrement the statistic by.
* @return boolean True if successful, false if not.
*/
public static function decr($stat, $by = 1)
{
return (bool)Resque::redis()->decrby('stat:' . $stat, $by);
}
/**
* Delete a statistic with the given name.
*
* @param string $stat The name of the statistic to delete.
* @return boolean True if successful, false if not.
*/
public static function clear($stat)
{
return (bool)Resque::redis()->del('stat:' . $stat);
}
}

View File

@ -0,0 +1,585 @@
<?php
require_once dirname(__FILE__) . '/Stat.php';
require_once dirname(__FILE__) . '/Event.php';
require_once dirname(__FILE__) . '/Job.php';
require_once dirname(__FILE__) . '/Job/DirtyExitException.php';
/**
* Resque worker that handles checking queues for jobs, fetching them
* off the queues, running them and handling the result.
*
* @package Resque/Worker
* @author Chris Boulton <chris@bigcommerce.com>
* @license http://www.opensource.org/licenses/mit-license.php
*/
class Resque_Worker
{
const LOG_NONE = 0;
const LOG_NORMAL = 1;
const LOG_VERBOSE = 2;
/**
* @var int Current log level of this worker.
*/
public $logLevel = 0;
/**
* @var array Array of all associated queues for this worker.
*/
private $queues = array();
/**
* @var string The hostname of this worker.
*/
private $hostname;
/**
* @var boolean True if on the next iteration, the worker should shutdown.
*/
private $shutdown = false;
/**
* @var boolean True if this worker is paused.
*/
private $paused = false;
/**
* @var string String identifying this worker.
*/
private $id;
/**
* @var Resque_Job Current job, if any, being processed by this worker.
*/
private $currentJob = null;
/**
* @var int Process ID of child worker processes.
*/
private $child = null;
/**
* Return all workers known to Resque as instantiated instances.
* @return array
*/
public static function all()
{
$workers = Resque::redis()->smembers('workers');
if(!is_array($workers)) {
$workers = array();
}
$instances = array();
foreach($workers as $workerId) {
$instances[] = self::find($workerId);
}
return $instances;
}
/**
* Given a worker ID, check if it is registered/valid.
*
* @param string $workerId ID of the worker.
* @return boolean True if the worker exists, false if not.
*/
public static function exists($workerId)
{
return (bool)Resque::redis()->sismember('workers', $workerId);
}
/**
* Given a worker ID, find it and return an instantiated worker class for it.
*
* @param string $workerId The ID of the worker.
* @return Resque_Worker Instance of the worker. False if the worker does not exist.
*/
public static function find($workerId)
{
if(!self::exists($workerId) || false === strpos($workerId, ":")) {
return false;
}
list($hostname, $pid, $queues) = explode(':', $workerId, 3);
$queues = explode(',', $queues);
$worker = new self($queues);
$worker->setId($workerId);
return $worker;
}
/**
* Set the ID of this worker to a given ID string.
*
* @param string $workerId ID for the worker.
*/
public function setId($workerId)
{
$this->id = $workerId;
}
/**
* Instantiate a new worker, given a list of queues that it should be working
* on. The list of queues should be supplied in the priority that they should
* be checked for jobs (first come, first served)
*
* Passing a single '*' allows the worker to work on all queues in alphabetical
* order. You can easily add new queues dynamically and have them worked on using
* this method.
*
* @param string|array $queues String with a single queue name, array with multiple.
*/
public function __construct($queues)
{
if(!is_array($queues)) {
$queues = array($queues);
}
$this->queues = $queues;
if(function_exists('gethostname')) {
$hostname = gethostname();
}
else {
$hostname = php_uname('n');
}
$this->hostname = $hostname;
$this->id = $this->hostname . ':'.getmypid() . ':' . implode(',', $this->queues);
}
/**
* The primary loop for a worker which when called on an instance starts
* the worker's life cycle.
*
* Queues are checked every $interval (seconds) for new jobs.
*
* @param int $interval How often to check for new jobs across the queues.
*/
public function work($interval = 5)
{
$this->updateProcLine('Starting');
$this->startup();
while(true) {
if($this->shutdown) {
break;
}
// Attempt to find and reserve a job
$job = false;
if(!$this->paused) {
$job = $this->reserve();
}
if(!$job) {
// For an interval of 0, break now - helps with unit testing etc
if($interval == 0) {
break;
}
// If no job was found, we sleep for $interval before continuing and checking again
$this->log('Sleeping for ' . $interval, true);
if($this->paused) {
$this->updateProcLine('Paused');
}
else {
$this->updateProcLine('Waiting for ' . implode(',', $this->queues));
}
usleep($interval * 1000000);
continue;
}
$this->log('got ' . $job);
Resque_Event::trigger('beforeFork', $job);
$this->workingOn($job);
$this->child = $this->fork();
// Forked and we're the child. Run the job.
if ($this->child === 0 || $this->child === false) {
$status = 'Processing ' . $job->queue . ' since ' . strftime('%F %T');
$this->updateProcLine($status);
$this->log($status, self::LOG_VERBOSE);
$this->perform($job);
if ($this->child === 0) {
exit(0);
}
}
if($this->child > 0) {
// Parent process, sit and wait
$status = 'Forked ' . $this->child . ' at ' . strftime('%F %T');
$this->updateProcLine($status);
$this->log($status, self::LOG_VERBOSE);
// Wait until the child process finishes before continuing
pcntl_wait($status);
$exitStatus = pcntl_wexitstatus($status);
if($exitStatus !== 0) {
$job->fail(new Resque_Job_DirtyExitException(
'Job exited with exit code ' . $exitStatus
));
}
}
$this->child = null;
$this->doneWorking();
}
$this->unregisterWorker();
}
/**
* Process a single job.
*
* @param Resque_Job $job The job to be processed.
*/
public function perform(Resque_Job $job)
{
try {
Resque_Event::trigger('afterFork', $job);
$job->perform();
}
catch(Exception $e) {
$this->log($job . ' failed: ' . $e->getMessage());
$job->fail($e);
return;
}
$job->updateStatus(Resque_Job_Status::STATUS_COMPLETE);
$this->log('done ' . $job);
}
/**
* Attempt to find a job from the top of one of the queues for this worker.
*
* @return object|boolean Instance of Resque_Job if a job is found, false if not.
*/
public function reserve()
{
$queues = $this->queues();
if(!is_array($queues)) {
return;
}
foreach($queues as $queue) {
$this->log('Checking ' . $queue, self::LOG_VERBOSE);
$job = Resque_Job::reserve($queue);
if($job) {
$this->log('Found job on ' . $queue, self::LOG_VERBOSE);
return $job;
}
}
return false;
}
/**
* Return an array containing all of the queues that this worker should use
* when searching for jobs.
*
* If * is found in the list of queues, every queue will be searched in
* alphabetic order. (@see $fetch)
*
* @param boolean $fetch If true, and the queue is set to *, will fetch
* all queue names from redis.
* @return array Array of associated queues.
*/
public function queues($fetch = true)
{
if(!in_array('*', $this->queues) || $fetch == false) {
return $this->queues;
}
$queues = Resque::queues();
sort($queues);
return $queues;
}
/**
* Attempt to fork a child process from the parent to run a job in.
*
* Return values are those of pcntl_fork().
*
* @return int -1 if the fork failed, 0 for the forked child, the PID of the child for the parent.
*/
private function fork()
{
if(!function_exists('pcntl_fork')) {
return false;
}
$pid = pcntl_fork();
if($pid === -1) {
throw new RuntimeException('Unable to fork child worker.');
}
return $pid;
}
/**
* Perform necessary actions to start a worker.
*/
private function startup()
{
$this->registerSigHandlers();
$this->pruneDeadWorkers();
Resque_Event::trigger('beforeFirstFork', $this);
$this->registerWorker();
}
/**
* On supported systems (with the PECL proctitle module installed), update
* the name of the currently running process to indicate the current state
* of a worker.
*
* @param string $status The updated process title.
*/
private function updateProcLine($status)
{
if(function_exists('setproctitle')) {
setproctitle('resque-' . Resque::VERSION . ': ' . $status);
}
}
/**
* Register signal handlers that a worker should respond to.
*
* TERM: Shutdown immediately and stop processing jobs.
* INT: Shutdown immediately and stop processing jobs.
* QUIT: Shutdown after the current job finishes processing.
* USR1: Kill the forked child immediately and continue processing jobs.
*/
private function registerSigHandlers()
{
if(!function_exists('pcntl_signal')) {
return;
}
declare(ticks = 1);
pcntl_signal(SIGTERM, array($this, 'shutDownNow'));
pcntl_signal(SIGINT, array($this, 'shutDownNow'));
pcntl_signal(SIGQUIT, array($this, 'shutdown'));
pcntl_signal(SIGUSR1, array($this, 'killChild'));
pcntl_signal(SIGUSR2, array($this, 'pauseProcessing'));
pcntl_signal(SIGCONT, array($this, 'unPauseProcessing'));
pcntl_signal(SIGPIPE, array($this, 'reestablishRedisConnection'));
$this->log('Registered signals', self::LOG_VERBOSE);
}
/**
* Signal handler callback for USR2, pauses processing of new jobs.
*/
public function pauseProcessing()
{
$this->log('USR2 received; pausing job processing');
$this->paused = true;
}
/**
* Signal handler callback for CONT, resumes worker allowing it to pick
* up new jobs.
*/
public function unPauseProcessing()
{
$this->log('CONT received; resuming job processing');
$this->paused = false;
}
/**
* Signal handler for SIGPIPE, in the event the redis connection has gone away.
* Attempts to reconnect to redis, or raises an Exception.
*/
public function reestablishRedisConnection()
{
$this->log('SIGPIPE received; attempting to reconnect');
Resque::redis()->establishConnection();
}
/**
* Schedule a worker for shutdown. Will finish processing the current job
* and when the timeout interval is reached, the worker will shut down.
*/
public function shutdown()
{
$this->shutdown = true;
$this->log('Exiting...');
}
/**
* Force an immediate shutdown of the worker, killing any child jobs
* currently running.
*/
public function shutdownNow()
{
$this->shutdown();
$this->killChild();
}
/**
* Kill a forked child job immediately. The job it is processing will not
* be completed.
*/
public function killChild()
{
if(!$this->child) {
$this->log('No child to kill.', self::LOG_VERBOSE);
return;
}
$this->log('Killing child at ' . $this->child, self::LOG_VERBOSE);
if(exec('ps -o pid,state -p ' . $this->child, $output, $returnCode) && $returnCode != 1) {
$this->log('Killing child at ' . $this->child, self::LOG_VERBOSE);
posix_kill($this->child, SIGKILL);
$this->child = null;
}
else {
$this->log('Child ' . $this->child . ' not found, restarting.', self::LOG_VERBOSE);
$this->shutdown();
}
}
/**
* Look for any workers which should be running on this server and if
* they're not, remove them from Redis.
*
* This is a form of garbage collection to handle cases where the
* server may have been killed and the Resque workers did not die gracefully
* and therefore leave state information in Redis.
*/
public function pruneDeadWorkers()
{
$workerPids = $this->workerPids();
$workers = self::all();
foreach($workers as $worker) {
if (is_object($worker)) {
list($host, $pid, $queues) = explode(':', (string)$worker, 3);
if($host != $this->hostname || in_array($pid, $workerPids) || $pid == getmypid()) {
continue;
}
$this->log('Pruning dead worker: ' . (string)$worker, self::LOG_VERBOSE);
$worker->unregisterWorker();
}
}
}
/**
* Return an array of process IDs for all of the Resque workers currently
* running on this machine.
*
* @return array Array of Resque worker process IDs.
*/
public function workerPids()
{
$pids = array();
exec('ps -A -o pid,command | grep [r]esque', $cmdOutput);
foreach($cmdOutput as $line) {
list($pids[],) = explode(' ', trim($line), 2);
}
return $pids;
}
/**
* Register this worker in Redis.
*/
public function registerWorker()
{
Resque::redis()->sadd('workers', $this);
Resque::redis()->set('worker:' . (string)$this . ':started', strftime('%a %b %d %H:%M:%S %Z %Y'));
}
/**
* Unregister this worker in Redis. (shutdown etc)
*/
public function unregisterWorker()
{
if(is_object($this->currentJob)) {
$this->currentJob->fail(new Resque_Job_DirtyExitException);
}
$id = (string)$this;
Resque::redis()->srem('workers', $id);
Resque::redis()->del('worker:' . $id);
Resque::redis()->del('worker:' . $id . ':started');
Resque_Stat::clear('processed:' . $id);
Resque_Stat::clear('failed:' . $id);
}
/**
* Tell Redis which job we're currently working on.
*
* @param object $job Resque_Job instance containing the job we're working on.
*/
public function workingOn(Resque_Job $job)
{
$job->worker = $this;
$this->currentJob = $job;
$job->updateStatus(Resque_Job_Status::STATUS_RUNNING);
$data = json_encode(array(
'queue' => $job->queue,
'run_at' => strftime('%a %b %d %H:%M:%S %Z %Y'),
'payload' => $job->payload
));
Resque::redis()->set('worker:' . $job->worker, $data);
}
/**
* Notify Redis that we've finished working on a job, clearing the working
* state and incrementing the job stats.
*/
public function doneWorking()
{
$this->currentJob = null;
Resque_Stat::incr('processed');
Resque_Stat::incr('processed:' . (string)$this);
Resque::redis()->del('worker:' . (string)$this);
}
/**
* Generate a string representation of this worker.
*
* @return string String identifier for this worker instance.
*/
public function __toString()
{
return $this->id;
}
/**
* Output a given log message to STDOUT.
*
* @param string $message Message to output.
*/
public function log($message)
{
if($this->logLevel == self::LOG_NORMAL) {
fwrite(STDOUT, "*** " . $message . "\n");
}
else if($this->logLevel == self::LOG_VERBOSE) {
fwrite(STDOUT, "** [" . strftime('%T %Y-%m-%d') . "] " . $message . "\n");
}
}
/**
* Return an object describing the job this worker is currently working on.
*
* @return object Object with details of current job.
*/
public function job()
{
$job = Resque::redis()->get('worker:' . $this);
if(!$job) {
return array();
}
else {
return json_decode($job, true);
}
}
/**
* Get a statistic belonging to this worker.
*
* @param string $stat Statistic to fetch.
* @return int Statistic value.
*/
public function getStat($stat)
{
return Resque_Stat::get($stat . ':' . $this);
}
}
?>

View File

@ -0,0 +1,79 @@
<?php
$QUEUE = getenv('QUEUE');
if(empty($QUEUE)) {
die("Set QUEUE env var containing the list of queues to work.\n");
}
require_once 'lib/Resque.php';
require_once 'lib/Resque/Worker.php';
$REDIS_BACKEND = getenv('REDIS_BACKEND');
if(!empty($REDIS_BACKEND)) {
Resque::setBackend($REDIS_BACKEND);
}
$logLevel = 0;
$LOGGING = getenv('LOGGING');
$VERBOSE = getenv('VERBOSE');
$VVERBOSE = getenv('VVERBOSE');
if(!empty($LOGGING) || !empty($VERBOSE)) {
$logLevel = Resque_Worker::LOG_NORMAL;
}
else if(!empty($VVERBOSE)) {
$logLevel = Resque_Worker::LOG_VERBOSE;
}
$APP_INCLUDE = getenv('APP_INCLUDE');
if($APP_INCLUDE) {
if(!file_exists($APP_INCLUDE)) {
die('APP_INCLUDE ('.$APP_INCLUDE.") does not exist.\n");
}
require_once $APP_INCLUDE;
}
$interval = 5;
$INTERVAL = getenv('INTERVAL');
if(!empty($INTERVAL)) {
$interval = $INTERVAL;
}
$count = 1;
$COUNT = getenv('COUNT');
if(!empty($COUNT) && $COUNT > 1) {
$count = $COUNT;
}
if($count > 1) {
for($i = 0; $i < $count; ++$i) {
$pid = pcntl_fork();
if($pid == -1) {
die("Could not fork worker ".$i."\n");
}
// Child, start the worker
else if(!$pid) {
$queues = explode(',', $QUEUE);
$worker = new Resque_Worker($queues);
$worker->logLevel = $logLevel;
fwrite(STDOUT, '*** Starting worker '.$worker."\n");
$worker->work($interval);
break;
}
}
}
// Start a single worker
else {
$queues = explode(',', $QUEUE);
$worker = new Resque_Worker($queues);
$worker->logLevel = $logLevel;
$PIDFILE = getenv('PIDFILE');
if ($PIDFILE) {
file_put_contents($PIDFILE, getmypid()) or
die('Could not write PID information to ' . $PIDFILE);
}
fwrite(STDOUT, '*** Starting worker '.$worker."\n");
$worker->work($interval);
}
?>

View File

@ -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.

View File

@ -0,0 +1,30 @@
<?php
namespace ConnectionManager\Extra;
use React\Socket\ConnectorInterface;
use React\EventLoop\LoopInterface;
use React\Promise\Timer;
class ConnectionManagerDelay implements ConnectorInterface
{
private $connectionManager;
private $delay;
private $loop;
public function __construct(ConnectorInterface $connectionManager, $delay, LoopInterface $loop)
{
$this->connectionManager = $connectionManager;
$this->delay = $delay;
$this->loop = $loop;
}
public function connect($uri)
{
$connectionManager = $this->connectionManager;
return Timer\resolve($this->delay, $this->loop)->then(function () use ($connectionManager, $uri) {
return $connectionManager->connect($uri);
});
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace ConnectionManager\Extra;
use React\Socket\ConnectorInterface;
use InvalidArgumentException;
use Exception;
use React\Promise\Promise;
use React\Promise\CancellablePromiseInterface;
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 CancellablePromiseInterface) {
$pending->cancel();
}
});
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace ConnectionManager\Extra;
use React\Socket\ConnectorInterface;
use React\EventLoop\LoopInterface;
use React\Promise\Timer;
class ConnectionManagerTimeout implements ConnectorInterface
{
private $connectionManager;
private $timeout;
private $loop;
public function __construct(ConnectorInterface $connectionManager, $timeout, LoopInterface $loop)
{
$this->connectionManager = $connectionManager;
$this->timeout = $timeout;
$this->loop = $loop;
}
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;
});
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace ConnectionManager\Extra\Multiple;
use ConnectionManager\Extra\Multiple\ConnectionManagerConsecutive;
use React\Promise;
use React\Promise\CancellablePromiseInterface;
class ConnectionManagerConcurrent extends ConnectionManagerConsecutive
{
public function connect($uri)
{
$all = array();
foreach ($this->managers as $connector) {
/* @var $connection 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 CancellablePromiseInterface) {
$promise->cancel();
}
// if promise resolves despite cancellation, immediately close stream
$promise->then(function ($stream) use ($conn) {
if ($stream !== $conn) {
$stream->close();
}
});
}
return $conn;
});
}
}

View File

@ -0,0 +1,62 @@
<?php
namespace ConnectionManager\Extra\Multiple;
use React\Socket\ConnectorInterface;
use React\Promise;
use UnderflowException;
use React\Promise\CancellablePromiseInterface;
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 CancellablePromiseInterface) {
$pending->cancel();
}
});
}
}

View 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);
}
}

View 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;
}
}

21
vendor/clue/http-proxy-react/LICENSE vendored Normal file
View File

@ -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.

View File

@ -0,0 +1,213 @@
<?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\ConnectorInterface;
use React\Socket\FixedUriConnector;
/**
* 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 $proxyAuth = '';
/**
* 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 In its most simple form, the given
* connector will be a \React\Socket\Connector if you want to connect to
* a given IP address.
* @throws InvalidArgumentException if the proxy URL is invalid
*/
public function __construct($proxyUrl, ConnectorInterface $connector)
{
// 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
);
}
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;
$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->proxyAuth = 'Proxy-Authorization: Basic ' . base64_encode(
rawurldecode($parts['user'] . ':' . (isset($parts['pass']) ? $parts['pass'] : ''))
) . "\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'));
}
$host = trim($parts['host'], '[]');
$port = $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'] = $parts['host'];
}
// append query string
$proxyUri .= '?' . http_build_query($args, '', '&');;
// append fragment from URI if given
if (isset($parts['fragment'])) {
$proxyUri .= '#' . $parts['fragment'];
}
$auth = $this->proxyAuth;
return $this->connector->connect($proxyUri)->then(function (ConnectionInterface $stream) use ($host, $port, $auth) {
$deferred = new Deferred(function ($_, $reject) use ($stream) {
$reject(new RuntimeException('Connection canceled while waiting for response from proxy (ECONNABORTED)', defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103));
$stream->close();
});
// keep buffering data until headers are complete
$buffer = '';
$fn = function ($chunk) use (&$buffer, $deferred, $stream) {
$buffer .= $chunk;
$pos = strpos($buffer, "\r\n\r\n");
if ($pos !== false) {
// try to parse headers as response message
try {
$response = Psr7\parse_response(substr($buffer, 0, $pos));
} catch (Exception $e) {
$deferred->reject(new RuntimeException('Invalid response received from proxy (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('Proxy denied connection due to invalid authentication ' . $response->getStatusCode() . ' (' . $response->getReasonPhrase() . ') (EACCES)', defined('SOCKET_EACCES') ? SOCKET_EACCES : 13));
return $stream->close();
} elseif ($response->getStatusCode() < 200 || $response->getStatusCode() >= 300) {
// map non-2xx status code to ECONNREFUSED
$deferred->reject(new RuntimeException('Proxy refused connection with HTTP error code ' . $response->getStatusCode() . ' (' . $response->getReasonPhrase() . ') (ECONNREFUSED)', defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111));
return $stream->close();
}
// 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('Proxy must not send more than 8 KiB of headers (EMSGSIZE)', defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 90));
$stream->close();
}
};
$stream->on('data', $fn);
$stream->on('error', function (Exception $e) use ($deferred) {
$deferred->reject(new RuntimeException('Stream error while waiting for response from proxy (EIO)', defined('SOCKET_EIO') ? SOCKET_EIO : 5, $e));
});
$stream->on('close', function () use ($deferred) {
$deferred->reject(new RuntimeException('Connection to proxy lost while waiting for response (ECONNRESET)', defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 104));
});
$stream->write("CONNECT " . $host . ":" . $port . " HTTP/1.1\r\nHost: " . $host . ":" . $port . "\r\n" . $auth . "\r\n");
return $deferred->promise()->then(function (ConnectionInterface $stream) use ($fn) {
// Stop buffering when connection has been established.
$stream->removeListener('data', $fn);
return new Promise\FulfilledPromise($stream);
});
}, function (Exception $e) use ($proxyUri) {
throw new RuntimeException('Unable to connect to proxy (ECONNREFUSED)', defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111, $e);
});
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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());
}
}

View File

@ -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);
}
}

View File

@ -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);
}

View 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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,10 @@
<?php
namespace Clue\Redis\Protocol\Parser;
use UnexpectedValueException;
class ParserException extends UnexpectedValueException
{
}

View 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);
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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);
}

21
vendor/clue/redis-react/LICENSE vendored Normal file
View File

@ -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.

52
vendor/clue/redis-react/src/Client.php vendored Normal file
View File

@ -0,0 +1,52 @@
<?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 resolved 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
*
* @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.
*
* @see self::end() for closing the connection once the client is idle
*/
public function close();
}

144
vendor/clue/redis-react/src/Factory.php vendored Normal file
View File

@ -0,0 +1,144 @@
<?php
namespace Clue\React\Redis;
use Clue\React\Redis\StreamingClient;
use Clue\Redis\Protocol\Factory as ProtocolFactory;
use React\EventLoop\LoopInterface;
use React\Promise;
use React\Socket\ConnectionInterface;
use React\Socket\Connector;
use React\Socket\ConnectorInterface;
use InvalidArgumentException;
class Factory
{
private $connector;
private $protocol;
/**
* @param LoopInterface $loop
* @param ConnectorInterface|null $connector [optional] Connector to use.
* Should be `null` in order to use default Connector.
* @param ProtocolFactory|null $protocol
*/
public function __construct(LoopInterface $loop, ConnectorInterface $connector = null, ProtocolFactory $protocol = null)
{
if ($connector === null) {
$connector = new Connector($loop);
}
if ($protocol === null) {
$protocol = new ProtocolFactory();
}
$this->connector = $connector;
$this->protocol = $protocol;
}
/**
* create redis client connected to address of given redis instance
*
* @param string $target Redis server URI to connect to
* @return \React\Promise\PromiseInterface resolves with Client or rejects with \Exception
*/
public function createClient($target)
{
try {
$parts = $this->parseUrl($target);
} catch (InvalidArgumentException $e) {
return Promise\reject($e);
}
$protocol = $this->protocol;
$promise = $this->connector->connect($parts['authority'])->then(function (ConnectionInterface $stream) use ($protocol) {
return new StreamingClient($stream, $protocol->createResponseParser(), $protocol->createSerializer());
});
if (isset($parts['auth'])) {
$promise = $promise->then(function (StreamingClient $client) use ($parts) {
return $client->auth($parts['auth'])->then(
function () use ($client) {
return $client;
},
function ($error) use ($client) {
$client->close();
throw $error;
}
);
});
}
if (isset($parts['db'])) {
$promise = $promise->then(function (StreamingClient $client) use ($parts) {
return $client->select($parts['db'])->then(
function () use ($client) {
return $client;
},
function ($error) use ($client) {
$client->close();
throw $error;
}
);
});
}
return $promise;
}
/**
* @param string $target
* @return array with keys authority, auth and db
* @throws InvalidArgumentException
*/
private function parseUrl($target)
{
$ret = array();
// support `redis+unix://` scheme for Unix domain socket (UDS) paths
if (preg_match('/^redis\+unix:\/\/([^:]*:[^@]*@)?(.+?)(\?.*)?$/', $target, $match)) {
$ret['authority'] = 'unix://' . $match[2];
$target = 'redis://' . (isset($match[1]) ? $match[1] : '') . 'localhost' . (isset($match[3]) ? $match[3] : '');
}
if (strpos($target, '://') === false) {
$target = 'redis://' . $target;
}
$parts = parse_url($target);
if ($parts === false || !isset($parts['scheme'], $parts['host']) || !in_array($parts['scheme'], array('redis', 'rediss'))) {
throw new InvalidArgumentException('Given URL can not be parsed');
}
if (isset($parts['pass'])) {
$ret['auth'] = rawurldecode($parts['pass']);
}
if (isset($parts['path']) && $parts['path'] !== '') {
// skip first slash
$ret['db'] = substr($parts['path'], 1);
}
if (!isset($ret['authority'])) {
$ret['authority'] =
($parts['scheme'] === 'rediss' ? 'tls://' : '') .
$parts['host'] . ':' .
(isset($parts['port']) ? $parts['port'] : 6379);
}
if (isset($parts['query'])) {
$args = array();
parse_str($parts['query'], $args);
if (isset($args['password'])) {
$ret['auth'] = $args['password'];
}
if (isset($args['db'])) {
$ret['db'] = $args['db'];
}
}
return $ret;
}
}

View File

@ -0,0 +1,180 @@
<?php
namespace Clue\React\Redis;
use Evenement\EventEmitter;
use Clue\Redis\Protocol\Parser\ParserInterface;
use Clue\Redis\Protocol\Parser\ParserException;
use Clue\Redis\Protocol\Serializer\SerializerInterface;
use Clue\Redis\Protocol\Factory as ProtocolFactory;
use UnderflowException;
use RuntimeException;
use InvalidArgumentException;
use React\Promise\Deferred;
use Clue\Redis\Protocol\Model\ErrorReply;
use Clue\Redis\Protocol\Model\ModelInterface;
use Clue\Redis\Protocol\Model\MultiBulkReply;
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($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 closed'));
} elseif (count($args) !== 1 && in_array($name, $pubsubs)) {
$request->reject(new InvalidArgumentException('PubSub commands limited to single argument'));
} elseif ($name === 'monitor') {
$request->reject(new \BadMethodCallException('MONITOR command explicitly not supported'));
} 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');
}
$request = array_shift($this->requests);
/* @var $request 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;
$this->stream->close();
$this->emit('close');
// reject all remaining requests in the queue
while($this->requests) {
$request = array_shift($this->requests);
/* @var $request Request */
$request->reject(new RuntimeException('Connection closing'));
}
}
}

21
vendor/clue/socket-raw/LICENSE vendored Normal file
View File

@ -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.

View File

@ -0,0 +1,80 @@
<?php
namespace Socket\Raw;
use RuntimeException;
class Exception extends RuntimeException
{
/**
* Create an Exception after a socket operation on the given $resource failed
*
* @param 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')
{
$code = socket_last_error($resource);
socket_clear_error($resource);
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;
}
}

273
vendor/clue/socket-raw/src/Factory.php vendored Normal file
View File

@ -0,0 +1,273 @@
<?php
namespace Socket\Raw;
use \InvalidArgumentException;
class Factory
{
/**
* create client socket connected to given target address
*
* @param string $address target address to connect to
* @return \Socket\Raw\Socket
* @throws InvalidArgumentException if given address is invalid
* @throws Exception on error
* @uses self::createFromString()
* @uses Socket::connect()
*/
public function createClient($address)
{
$socket = $this->createFromString($address, $scheme);
try {
$socket->connect($address);
}
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
* @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
* @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
* @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 $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;
}
}

531
vendor/clue/socket-raw/src/Socket.php vendored Normal file
View File

@ -0,0 +1,531 @@
<?php
namespace Socket\Raw;
/**
* simple and lightweight OOP wrapper for the low level sockets extension (ext-sockets)
*
* @author clue
* @link https://github.com/clue/socket-raw
*/
class Socket
{
/**
* reference to actual socket resource
*
* @var resource
*/
private $resource;
/**
* instanciate socket wrapper for given socket resource
*
* should usually not be called manually, see Factory
*
* @param 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 resource
*/
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
* @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
* @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)
* @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
* @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
* @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 => otherwise re-throw
if ($e->getCode() !== SOCKET_EINPROGRESS) {
throw $e;
}
// connection should be completed (or rejected) within timeout
if ($this->selectWrite($timeout) === false) {
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
* @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
* @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
* @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
* @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
* @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
* @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
* @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
* @uses socket_select()
*/
public function selectRead($sec = 0)
{
$usec = $sec === null ? null : (($sec - floor($sec)) * 1000000);
$r = array($this->resource);
$ret = @socket_select($r, $x, $x, $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
* @uses socket_select()
*/
public function selectWrite($sec = 0)
{
$usec = $sec === null ? null : (($sec - floor($sec)) * 1000000);
$w = array($this->resource);
$ret = @socket_select($x, $w, $x, $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
* @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
* @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
* @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
* @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
* @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
* @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
* @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
* @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;
}
}

21
vendor/clue/socks-react/LICENSE vendored Normal file
View File

@ -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.

382
vendor/clue/socks-react/src/Client.php vendored Normal file
View File

@ -0,0 +1,382 @@
<?php
namespace Clue\React\Socks;
use React\Promise;
use React\Promise\PromiseInterface;
use React\Promise\Deferred;
use React\Socket\ConnectionInterface;
use React\Socket\ConnectorInterface;
use React\Socket\FixedUriConnector;
use \Exception;
use \InvalidArgumentException;
use RuntimeException;
class Client implements ConnectorInterface
{
/**
*
* @var ConnectorInterface
*/
private $connector;
private $socksUri;
private $protocolVersion = null;
private $auth = null;
public function __construct($socksUri, ConnectorInterface $connector)
{
// support `sockss://` scheme for SOCKS over TLS
// support `socks+unix://` scheme for Unix domain socket (UDS) paths
if (preg_match('/^(socks(?:5|4|4a)?)(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
);
}
// 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') {
// default to using SOCKS5 if not given explicitly
$parts['scheme'] = 'socks5';
} elseif ($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;
}
private function setProtocolVersionFromScheme($scheme)
{
if ($scheme === 'socks' || $scheme === 'socks4a') {
$this->protocolVersion = '4a';
} elseif ($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, $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 React'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 ($this->protocolVersion === '4' && false === filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
return Promise\reject(new InvalidArgumentException('Requires an IPv4 address for SOCKS4'));
}
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'];
}
$that = $this;
// start TCP/IP connection to SOCKS server and then
// handle SOCKS protocol once connection is ready
// resolve plain connection once SOCKS protocol is completed
return $this->connector->connect($socksUri)->then(
function (ConnectionInterface $stream) use ($that, $host, $port) {
return $that->handleConnectedSocks($stream, $host, $port);
},
function (Exception $e) {
throw new RuntimeException('Unable to connect to proxy (ECONNREFUSED)', defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111, $e);
}
);
}
/**
* Internal helper used to handle the communication with the SOCKS server
*
* @param ConnectionInterface $stream
* @param string $host
* @param int $port
* @return Promise Promise<ConnectionInterface, Exception>
* @internal
*/
public function handleConnectedSocks(ConnectionInterface $stream, $host, $port)
{
$deferred = new Deferred(function ($_, $reject) {
$reject(new RuntimeException('Connection canceled while establishing SOCKS session (ECONNABORTED)', defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103));
});
$reader = new StreamReader();
$stream->on('data', array($reader, 'write'));
$stream->on('error', $onError = function (Exception $e) use ($deferred) {
$deferred->reject(new RuntimeException('Stream error while waiting for response from proxy (EIO)', defined('SOCKET_EIO') ? SOCKET_EIO : 5, $e));
});
$stream->on('close', $onClose = function () use ($deferred) {
$deferred->reject(new RuntimeException('Connection to proxy lost while waiting for response (ECONNRESET)', defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 104));
});
if ($this->protocolVersion === '5') {
$promise = $this->handleSocks5($stream, $host, $port, $reader);
} else {
$promise = $this->handleSocks4($stream, $host, $port, $reader);
}
$promise->then(function () use ($deferred, $stream) {
$deferred->resolve($stream);
}, function (Exception $error) use ($deferred) {
// pass custom RuntimeException through as-is, otherwise wrap in protocol error
if (!$error instanceof RuntimeException) {
$error = new RuntimeException('Invalid response received from proxy (EBADMSG)', defined('SOCKET_EBADMSG') ? SOCKET_EBADMSG: 71, $error);
}
$deferred->reject($error);
});
return $deferred->promise()->then(
function (ConnectionInterface $stream) use ($reader, $onError, $onClose) {
$stream->removeListener('data', array($reader, 'write'));
$stream->removeListener('error', $onError);
$stream->removeListener('close', $onClose);
return $stream;
},
function ($error) use ($stream, $onClose) {
$stream->removeListener('close', $onClose);
$stream->close();
throw $error;
}
);
}
private function handleSocks4(ConnectionInterface $stream, $host, $port, StreamReader $reader)
{
// 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) {
if ($data['null'] !== 0x00) {
throw new Exception('Invalid SOCKS response');
}
if ($data['status'] !== 0x5a) {
throw new RuntimeException('Proxy refused connection with SOCKS error code ' . sprintf('0x%02X', $data['status']) . ' (ECONNREFUSED)', defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111);
}
});
}
private function handleSocks5(ConnectionInterface $stream, $host, $port, StreamReader $reader)
{
// 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) {
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) {
if ($data['version'] !== 0x01 || $data['status'] !== 0x00) {
throw new RuntimeException('Username/Password authentication failed (EACCES)', defined('SOCKET_EACCES') ? SOCKET_EACCES : 13);
}
});
} else if ($data['method'] !== 0x00) {
// any other method than "no authentication"
throw new RuntimeException('No acceptable authentication method found (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) {
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('SOCKS server reported a general server failure (ECONNREFUSED)', defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111);
} elseif ($data['status'] === Server::ERROR_NOT_ALLOWED_BY_RULESET) {
throw new RuntimeException('SOCKS server reported connection is not allowed by ruleset (EACCES)', defined('SOCKET_EACCES') ? SOCKET_EACCES : 13);
} elseif ($data['status'] === Server::ERROR_NETWORK_UNREACHABLE) {
throw new RuntimeException('SOCKS server reported network unreachable (ENETUNREACH)', defined('SOCKET_ENETUNREACH') ? SOCKET_ENETUNREACH : 101);
} elseif ($data['status'] === Server::ERROR_HOST_UNREACHABLE) {
throw new RuntimeException('SOCKS server reported host unreachable (EHOSTUNREACH)', defined('SOCKET_EHOSTUNREACH') ? SOCKET_EHOSTUNREACH : 113);
} elseif ($data['status'] === Server::ERROR_CONNECTION_REFUSED) {
throw new RuntimeException('SOCKS server reported connection refused (ECONNREFUSED)', defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111);
} elseif ($data['status'] === Server::ERROR_TTL) {
throw new RuntimeException('SOCKS server reported TTL/timeout expired (ETIMEDOUT)', defined('SOCKET_ETIMEDOUT') ? SOCKET_ETIMEDOUT : 110);
} elseif ($data['status'] === Server::ERROR_COMMAND_UNSUPPORTED) {
throw new RuntimeException('SOCKS server does not support the CONNECT command (EPROTO)', defined('SOCKET_EPROTO') ? SOCKET_EPROTO : 71);
} elseif ($data['status'] === Server::ERROR_ADDRESS_UNSUPPORTED) {
throw new RuntimeException('SOCKS server does not support this address type (EPROTO)', defined('SOCKET_EPROTO') ? SOCKET_EPROTO : 71);
}
throw new RuntimeException('SOCKS server reported an unassigned 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');
}
});
}
}

422
vendor/clue/socks-react/src/Server.php vendored Normal file
View File

@ -0,0 +1,422 @@
<?php
namespace Clue\React\Socks;
use Evenement\EventEmitter;
use React\Socket\ServerInterface;
use React\Promise;
use React\Promise\Deferred;
use React\Promise\PromiseInterface;
use React\Socket\ConnectorInterface;
use React\Socket\Connector;
use React\Socket\ConnectionInterface;
use React\EventLoop\LoopInterface;
use \UnexpectedValueException;
use \InvalidArgumentException;
use \Exception;
use React\Promise\Timer\TimeoutException;
class Server extends EventEmitter
{
// 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;
protected $loop;
private $connector;
private $auth = null;
private $protocolVersion = null;
public function __construct(LoopInterface $loop, ServerInterface $serverInterface, ConnectorInterface $connector = null)
{
if ($connector === null) {
$connector = new Connector($loop);
}
$this->loop = $loop;
$this->connector = $connector;
$that = $this;
$serverInterface->on('connection', function ($connection) use ($that) {
$that->emit('connection', array($connection));
$that->onConnection($connection);
});
}
public function setProtocolVersion($version)
{
if ($version !== null) {
$version = (string)$version;
if (!in_array($version, array('4', '4a', '5'), true)) {
throw new InvalidArgumentException('Invalid protocol version given');
}
if ($version !== '5' && $this->auth !== null){
throw new UnexpectedValueException('Unable to change protocol version to anything but SOCKS5 while authentication is used. Consider removing authentication info or sticking to SOCKS5');
}
}
$this->protocolVersion = $version;
}
public function setAuth($auth)
{
if (!is_callable($auth)) {
throw new InvalidArgumentException('Given authenticator is not a valid callable');
}
if ($this->protocolVersion !== null && $this->protocolVersion !== '5') {
throw new UnexpectedValueException('Authentication requires SOCKS5. Consider using protocol version 5 or waive authentication');
}
// wrap authentication callback in order to cast its return value to a promise
$this->auth = function($username, $password, $remote) use ($auth) {
$ret = call_user_func($auth, $username, $password, $remote);
if ($ret instanceof PromiseInterface) {
return $ret;
}
$deferred = new Deferred();
$ret ? $deferred->resolve() : $deferred->reject();
return $deferred->promise();
};
}
public function setAuthArray(array $login)
{
$this->setAuth(function ($username, $password) use ($login) {
return (isset($login[$username]) && (string)$login[$username] === $password);
});
}
public function unsetAuth()
{
$this->auth = null;
}
public function onConnection(ConnectionInterface $connection)
{
$that = $this;
$handling = $this->handleSocks($connection)->then(function($remote) use ($connection){
$connection->emit('ready',array($remote));
}, function ($error) use ($connection, $that) {
if (!($error instanceof \Exception)) {
$error = new \Exception($error);
}
$connection->emit('error', array($error));
$that->endConnection($connection);
});
$connection->on('close', function () use ($handling) {
$handling->cancel();
});
}
/**
* gracefully shutdown connection by flushing all remaining data and closing stream
*/
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;
$that = $this;
$auth = $this->auth;
$protocolVersion = $this->protocolVersion;
// authentication requires SOCKS5
if ($auth !== null) {
$protocolVersion = '5';
}
return $reader->readByte()->then(function ($version) use ($stream, $that, $protocolVersion, $auth, $reader){
if ($version === 0x04) {
if ($protocolVersion === '5') {
throw new UnexpectedValueException('SOCKS4 not allowed due to configuration');
}
return $that->handleSocks4($stream, $protocolVersion, $reader);
} else if ($version === 0x05) {
if ($protocolVersion !== null && $protocolVersion !== '5') {
throw new UnexpectedValueException('SOCKS5 not allowed due to configuration');
}
return $that->handleSocks5($stream, $auth, $reader);
}
throw new UnexpectedValueException('Unexpected/unknown version number');
});
}
public function handleSocks4(ConnectionInterface $stream, $protocolVersion, StreamReader $reader)
{
// suppliying hostnames is only allowed for SOCKS4a (or automatically detected version)
$supportsHostname = ($protocolVersion === null || $protocolVersion === '4a');
$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, $supportsHostname, $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 && $supportsHostname) {
// 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);
});
}
public function handleSocks5(ConnectionInterface $stream, $auth=null, 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 = 'socks5' . ($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 ($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 () use ($stream, $username) {
// accept
$stream->emit('auth', array($username));
$stream->write(pack('C2', 0x01, 0x00));
}, function() use ($stream) {
// reject => send any code but 0x00
$stream->end(pack('C2', 0x01, 0xFF));
throw new UnexpectedValueException('Unable to authenticate');
});
});
});
} 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, $stream) {
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;
});
}
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 Promise\reject(new InvalidArgumentException('Invalid target URI given'));
}
if (isset($target[2])) {
$uri .= '?source=' . rawurlencode($target[2]);
}
$stream->emit('target', $target);
$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) {
$stream->emit('shutdown', array('remote', null));
$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);
});
}
}

View File

@ -0,0 +1,149 @@
<?php
namespace Clue\React\Socks;
use React\Promise\Deferred;
use \InvalidArgumentException;
use \UnexpectedValueException;
/**
* @internal
*/
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;
}
}

445
vendor/composer/ClassLoader.php vendored Normal file
View File

@ -0,0 +1,445 @@
<?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 http://www.php-fig.org/psr/psr-0/
* @see http://www.php-fig.org/psr/psr-4/
*/
class ClassLoader
{
// PSR-4
private $prefixLengthsPsr4 = array();
private $prefixDirsPsr4 = array();
private $fallbackDirsPsr4 = array();
// PSR-0
private $prefixesPsr0 = array();
private $fallbackDirsPsr0 = array();
private $useIncludePath = false;
private $classMap = array();
private $classMapAuthoritative = false;
private $missingClasses = array();
private $apcuPrefix;
public function getPrefixes()
{
if (!empty($this->prefixesPsr0)) {
return call_user_func_array('array_merge', $this->prefixesPsr0);
}
return array();
}
public function getPrefixesPsr4()
{
return $this->prefixDirsPsr4;
}
public function getFallbackDirs()
{
return $this->fallbackDirsPsr0;
}
public function getFallbackDirsPsr4()
{
return $this->fallbackDirsPsr4;
}
public function getClassMap()
{
return $this->classMap;
}
/**
* @param array $classMap Class to filename map
*/
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 array|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
*/
public function add($prefix, $paths, $prepend = false)
{
if (!$prefix) {
if ($prepend) {
$this->fallbackDirsPsr0 = array_merge(
(array) $paths,
$this->fallbackDirsPsr0
);
} else {
$this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0,
(array) $paths
);
}
return;
}
$first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) {
$this->prefixesPsr0[$first][$prefix] = (array) $paths;
return;
}
if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge(
(array) $paths,
$this->prefixesPsr0[$first][$prefix]
);
} else {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix],
(array) $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 array|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories
*
* @throws \InvalidArgumentException
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
(array) $paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
(array) $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] = (array) $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
(array) $paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
(array) $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 array|string $paths The PSR-0 base directories
*/
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 array|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*/
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
*/
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
*/
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
*/
public function setApcuPrefix($apcuPrefix)
{
$this->apcuPrefix = function_exists('apcu_fetch') && ini_get('apc.enabled') ? $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
*/
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
}
/**
* Unregisters this instance as an autoloader.
*/
public function unregister()
{
spl_autoload_unregister(array($this, 'loadClass'));
}
/**
* Loads the given class or interface.
*
* @param string $class The name of the class
* @return bool|null True if loaded, null otherwise
*/
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
includeFile($file);
return true;
}
}
/**
* 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;
}
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])) {
foreach ($this->prefixDirsPsr4[$search] as $dir) {
$length = $this->prefixLengthsPsr4[$first][$search];
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) {
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;
}
}
/**
* Scope isolated include.
*
* Prevents access to $this/self from included files.
*/
function includeFile($file)
{
include $file;
}

21
vendor/composer/LICENSE vendored Normal file
View File

@ -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.

9
vendor/composer/autoload_classmap.php vendored Normal file
View File

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

20
vendor/composer/autoload_files.php vendored Normal file
View File

@ -0,0 +1,20 @@
<?php
// autoload_files.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'ad155f8f1cf0d418fe49e248db8c661b' => $vendorDir . '/react/promise/src/functions_include.php',
'6b06ce8ccf69c43a60a1e48495a034c9' => $vendorDir . '/react/promise-timer/src/functions.php',
'383eaff206634a77a1be54e64e6459c7' => $vendorDir . '/sabre/uri/lib/functions.php',
'ebf8799635f67b5d7248946fe2154f4a' => $vendorDir . '/ringcentral/psr7/src/functions_include.php',
'3569eecfeed3bcf0bad3c998a494ecb8' => $vendorDir . '/sabre/xml/lib/Deserializer/functions.php',
'93aa591bc4ca510c520999e34229ee79' => $vendorDir . '/sabre/xml/lib/Serializer/functions.php',
'2b9d0f43f9552984cfa82fee95491826' => $vendorDir . '/sabre/event/lib/coroutine.php',
'd81bab31d3feb45bfe2f283ea3c8fdf7' => $vendorDir . '/sabre/event/lib/Loop/functions.php',
'a1cce3d26cc15c00fcd0b3354bd72c88' => $vendorDir . '/sabre/event/lib/Promise/functions.php',
'cea474b4340aa9fa53661e887a21a316' => $vendorDir . '/react/promise-stream/src/functions_include.php',
'ebdb698ed4152ae445614b69b5e4bb6a' => $vendorDir . '/sabre/http/lib/functions.php',
);

13
vendor/composer/autoload_namespaces.php vendored Normal file
View File

@ -0,0 +1,13 @@
<?php
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'Resque' => array($vendorDir . '/chrisboulton/php-resque/lib'),
'MKraemer' => array($vendorDir . '/mkraemer/react-pcntl/src'),
'Evenement' => array($vendorDir . '/evenement/evenement/src'),
'Clue\\Redis\\Protocol' => array($vendorDir . '/clue/redis-protocol/src'),
);

42
vendor/composer/autoload_psr4.php vendored Normal file
View File

@ -0,0 +1,42 @@
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'Socket\\Raw\\' => array($vendorDir . '/clue/socket-raw/src'),
'Sabre\\Xml\\' => array($vendorDir . '/sabre/xml/lib'),
'Sabre\\VObject\\' => array($vendorDir . '/sabre/vobject/lib'),
'Sabre\\Uri\\' => array($vendorDir . '/sabre/uri/lib'),
'Sabre\\HTTP\\' => array($vendorDir . '/sabre/http/lib'),
'Sabre\\Event\\' => array($vendorDir . '/sabre/event/lib'),
'Sabre\\DAV\\' => array($vendorDir . '/sabre/dav/lib/DAV'),
'Sabre\\DAVACL\\' => array($vendorDir . '/sabre/dav/lib/DAVACL'),
'Sabre\\CardDAV\\' => array($vendorDir . '/sabre/dav/lib/CardDAV'),
'Sabre\\CalDAV\\' => array($vendorDir . '/sabre/dav/lib/CalDAV'),
'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'),
'Psr\\Log\\' => array($vendorDir . '/psr/log/Psr/Log'),
'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-message/src'),
'Predis\\' => array($vendorDir . '/predis/predis/src'),
'GuzzleHttp\\Stream\\' => array($vendorDir . '/guzzlehttp/streams/src'),
'GuzzleHttp\\Ring\\' => array($vendorDir . '/guzzlehttp/ringphp/src'),
'GuzzleHttp\\' => array($vendorDir . '/guzzlehttp/guzzle/src'),
'ConnectionManager\\Extra\\' => array($vendorDir . '/clue/connection-manager-extra/src'),
'Clue\\React\\Socks\\' => array($vendorDir . '/clue/socks-react/src'),
'Clue\\React\\Redis\\' => array($vendorDir . '/clue/redis-react/src'),
'Clue\\React\\HttpProxy\\' => array($vendorDir . '/clue/http-proxy-react/src'),
);

70
vendor/composer/autoload_real.php vendored Normal file
View File

@ -0,0 +1,70 @@
<?php
// autoload_real.php @generated by Composer
class ComposerAutoloaderInit3214150501998a72ea353f9c1a1e903e
{
private static $loader;
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
spl_autoload_register(array('ComposerAutoloaderInit3214150501998a72ea353f9c1a1e903e', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
spl_autoload_unregister(array('ComposerAutoloaderInit3214150501998a72ea353f9c1a1e903e', 'loadClassLoader'));
$useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
if ($useStaticLoader) {
require_once __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInit3214150501998a72ea353f9c1a1e903e::getInitializer($loader));
} else {
$map = require __DIR__ . '/autoload_namespaces.php';
foreach ($map as $namespace => $path) {
$loader->set($namespace, $path);
}
$map = require __DIR__ . '/autoload_psr4.php';
foreach ($map as $namespace => $path) {
$loader->setPsr4($namespace, $path);
}
$classMap = require __DIR__ . '/autoload_classmap.php';
if ($classMap) {
$loader->addClassMap($classMap);
}
}
$loader->register(true);
if ($useStaticLoader) {
$includeFiles = Composer\Autoload\ComposerStaticInit3214150501998a72ea353f9c1a1e903e::$files;
} else {
$includeFiles = require __DIR__ . '/autoload_files.php';
}
foreach ($includeFiles as $fileIdentifier => $file) {
composerRequire3214150501998a72ea353f9c1a1e903e($fileIdentifier, $file);
}
return $loader;
}
}
function composerRequire3214150501998a72ea353f9c1a1e903e($fileIdentifier, $file)
{
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
require $file;
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
}
}

249
vendor/composer/autoload_static.php vendored Normal file
View File

@ -0,0 +1,249 @@
<?php
// autoload_static.php @generated by Composer
namespace Composer\Autoload;
class ComposerStaticInit3214150501998a72ea353f9c1a1e903e
{
public static $files = array (
'ad155f8f1cf0d418fe49e248db8c661b' => __DIR__ . '/..' . '/react/promise/src/functions_include.php',
'6b06ce8ccf69c43a60a1e48495a034c9' => __DIR__ . '/..' . '/react/promise-timer/src/functions.php',
'383eaff206634a77a1be54e64e6459c7' => __DIR__ . '/..' . '/sabre/uri/lib/functions.php',
'ebf8799635f67b5d7248946fe2154f4a' => __DIR__ . '/..' . '/ringcentral/psr7/src/functions_include.php',
'3569eecfeed3bcf0bad3c998a494ecb8' => __DIR__ . '/..' . '/sabre/xml/lib/Deserializer/functions.php',
'93aa591bc4ca510c520999e34229ee79' => __DIR__ . '/..' . '/sabre/xml/lib/Serializer/functions.php',
'2b9d0f43f9552984cfa82fee95491826' => __DIR__ . '/..' . '/sabre/event/lib/coroutine.php',
'd81bab31d3feb45bfe2f283ea3c8fdf7' => __DIR__ . '/..' . '/sabre/event/lib/Loop/functions.php',
'a1cce3d26cc15c00fcd0b3354bd72c88' => __DIR__ . '/..' . '/sabre/event/lib/Promise/functions.php',
'cea474b4340aa9fa53661e887a21a316' => __DIR__ . '/..' . '/react/promise-stream/src/functions_include.php',
'ebdb698ed4152ae445614b69b5e4bb6a' => __DIR__ . '/..' . '/sabre/http/lib/functions.php',
);
public static $prefixLengthsPsr4 = array (
'S' =>
array (
'Socket\\Raw\\' => 11,
'Sabre\\Xml\\' => 10,
'Sabre\\VObject\\' => 14,
'Sabre\\Uri\\' => 10,
'Sabre\\HTTP\\' => 11,
'Sabre\\Event\\' => 12,
'Sabre\\DAV\\' => 10,
'Sabre\\DAVACL\\' => 13,
'Sabre\\CardDAV\\' => 14,
'Sabre\\CalDAV\\' => 13,
),
'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,
),
'P' =>
array (
'Psr\\Log\\' => 8,
'Psr\\Http\\Message\\' => 17,
'Predis\\' => 7,
),
'G' =>
array (
'GuzzleHttp\\Stream\\' => 18,
'GuzzleHttp\\Ring\\' => 16,
'GuzzleHttp\\' => 11,
),
'C' =>
array (
'ConnectionManager\\Extra\\' => 24,
'Clue\\React\\Socks\\' => 17,
'Clue\\React\\Redis\\' => 17,
'Clue\\React\\HttpProxy\\' => 21,
),
);
public static $prefixDirsPsr4 = array (
'Socket\\Raw\\' =>
array (
0 => __DIR__ . '/..' . '/clue/socket-raw/src',
),
'Sabre\\Xml\\' =>
array (
0 => __DIR__ . '/..' . '/sabre/xml/lib',
),
'Sabre\\VObject\\' =>
array (
0 => __DIR__ . '/..' . '/sabre/vobject/lib',
),
'Sabre\\Uri\\' =>
array (
0 => __DIR__ . '/..' . '/sabre/uri/lib',
),
'Sabre\\HTTP\\' =>
array (
0 => __DIR__ . '/..' . '/sabre/http/lib',
),
'Sabre\\Event\\' =>
array (
0 => __DIR__ . '/..' . '/sabre/event/lib',
),
'Sabre\\DAV\\' =>
array (
0 => __DIR__ . '/..' . '/sabre/dav/lib/DAV',
),
'Sabre\\DAVACL\\' =>
array (
0 => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL',
),
'Sabre\\CardDAV\\' =>
array (
0 => __DIR__ . '/..' . '/sabre/dav/lib/CardDAV',
),
'Sabre\\CalDAV\\' =>
array (
0 => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV',
),
'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',
),
'Psr\\Log\\' =>
array (
0 => __DIR__ . '/..' . '/psr/log/Psr/Log',
),
'Psr\\Http\\Message\\' =>
array (
0 => __DIR__ . '/..' . '/psr/http-message/src',
),
'Predis\\' =>
array (
0 => __DIR__ . '/..' . '/predis/predis/src',
),
'GuzzleHttp\\Stream\\' =>
array (
0 => __DIR__ . '/..' . '/guzzlehttp/streams/src',
),
'GuzzleHttp\\Ring\\' =>
array (
0 => __DIR__ . '/..' . '/guzzlehttp/ringphp/src',
),
'GuzzleHttp\\' =>
array (
0 => __DIR__ . '/..' . '/guzzlehttp/guzzle/src',
),
'ConnectionManager\\Extra\\' =>
array (
0 => __DIR__ . '/..' . '/clue/connection-manager-extra/src',
),
'Clue\\React\\Socks\\' =>
array (
0 => __DIR__ . '/..' . '/clue/socks-react/src',
),
'Clue\\React\\Redis\\' =>
array (
0 => __DIR__ . '/..' . '/clue/redis-react/src',
),
'Clue\\React\\HttpProxy\\' =>
array (
0 => __DIR__ . '/..' . '/clue/http-proxy-react/src',
),
);
public static $prefixesPsr0 = array (
'R' =>
array (
'Resque' =>
array (
0 => __DIR__ . '/..' . '/chrisboulton/php-resque/lib',
),
),
'M' =>
array (
'MKraemer' =>
array (
0 => __DIR__ . '/..' . '/mkraemer/react-pcntl/src',
),
),
'E' =>
array (
'Evenement' =>
array (
0 => __DIR__ . '/..' . '/evenement/evenement/src',
),
),
'C' =>
array (
'Clue\\Redis\\Protocol' =>
array (
0 => __DIR__ . '/..' . '/clue/redis-protocol/src',
),
),
);
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInit3214150501998a72ea353f9c1a1e903e::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInit3214150501998a72ea353f9c1a1e903e::$prefixDirsPsr4;
$loader->prefixesPsr0 = ComposerStaticInit3214150501998a72ea353f9c1a1e903e::$prefixesPsr0;
}, null, ClassLoader::class);
}
}

19
vendor/evenement/evenement/LICENSE vendored Normal file
View File

@ -0,0 +1,19 @@
Copyright (c) 2011 Igor Wiedler
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.

View File

@ -0,0 +1,17 @@
<?php
/*
* This file is part of Evenement.
*
* (c) Igor Wiedler <igor@wiedler.ch>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Evenement;
class EventEmitter implements EventEmitterInterface
{
use EventEmitterTrait;
}

View File

@ -0,0 +1,22 @@
<?php
/*
* This file is part of Evenement.
*
* (c) Igor Wiedler <igor@wiedler.ch>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Evenement;
interface EventEmitterInterface
{
public function on($event, callable $listener);
public function once($event, callable $listener);
public function removeListener($event, callable $listener);
public function removeAllListeners($event = null);
public function listeners($event);
public function emit($event, array $arguments = []);
}

View File

@ -0,0 +1,73 @@
<?php
/*
* This file is part of Evenement.
*
* (c) Igor Wiedler <igor@wiedler.ch>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Evenement;
trait EventEmitterTrait
{
protected $listeners = [];
public function on($event, callable $listener)
{
if (!isset($this->listeners[$event])) {
$this->listeners[$event] = [];
}
$this->listeners[$event][] = $listener;
return $this;
}
public function once($event, callable $listener)
{
$onceListener = function () use (&$onceListener, $event, $listener) {
$this->removeListener($event, $onceListener);
\call_user_func_array($listener, \func_get_args());
};
$this->on($event, $onceListener);
}
public function removeListener($event, callable $listener)
{
if (isset($this->listeners[$event])) {
$index = \array_search($listener, $this->listeners[$event], true);
if (false !== $index) {
unset($this->listeners[$event][$index]);
if (\count($this->listeners[$event]) === 0) {
unset($this->listeners[$event]);
}
}
}
}
public function removeAllListeners($event = null)
{
if ($event !== null) {
unset($this->listeners[$event]);
} else {
$this->listeners = [];
}
}
public function listeners($event)
{
return isset($this->listeners[$event]) ? $this->listeners[$event] : [];
}
public function emit($event, array $arguments = [])
{
foreach ($this->listeners($event) as $listener) {
\call_user_func_array($listener, $arguments);
}
}
}

19
vendor/guzzlehttp/guzzle/LICENSE vendored Normal file
View File

@ -0,0 +1,19 @@
Copyright (c) 2011-2015 Michael Dowling, https://github.com/mtdowling <mtdowling@gmail.com>
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.

View File

@ -0,0 +1,148 @@
<?php
namespace GuzzleHttp;
/**
* Represents the result of a batch operation. This result container is
* iterable, countable, and you can can get a result by value using the
* getResult function.
*
* Successful results are anything other than exceptions. Failure results are
* exceptions.
*
* @package GuzzleHttp
*/
class BatchResults implements \Countable, \IteratorAggregate, \ArrayAccess
{
private $hash;
/**
* @param \SplObjectStorage $hash Hash of key objects to result values.
*/
public function __construct(\SplObjectStorage $hash)
{
$this->hash = $hash;
}
/**
* Get the keys that are available on the batch result.
*
* @return array
*/
public function getKeys()
{
return iterator_to_array($this->hash);
}
/**
* Gets a result from the container for the given object. When getting
* results for a batch of requests, provide the request object.
*
* @param object $forObject Object to retrieve the result for.
*
* @return mixed|null
*/
public function getResult($forObject)
{
return isset($this->hash[$forObject]) ? $this->hash[$forObject] : null;
}
/**
* Get an array of successful results.
*
* @return array
*/
public function getSuccessful()
{
$results = [];
foreach ($this->hash as $key) {
if (!($this->hash[$key] instanceof \Exception)) {
$results[] = $this->hash[$key];
}
}
return $results;
}
/**
* Get an array of failed results.
*
* @return array
*/
public function getFailures()
{
$results = [];
foreach ($this->hash as $key) {
if ($this->hash[$key] instanceof \Exception) {
$results[] = $this->hash[$key];
}
}
return $results;
}
/**
* Allows iteration over all batch result values.
*
* @return \ArrayIterator
*/
public function getIterator()
{
$results = [];
foreach ($this->hash as $key) {
$results[] = $this->hash[$key];
}
return new \ArrayIterator($results);
}
/**
* Counts the number of elements in the batch result.
*
* @return int
*/
public function count()
{
return count($this->hash);
}
/**
* Checks if the batch contains a specific numerical array index.
*
* @param int $key Index to access
*
* @return bool
*/
public function offsetExists($key)
{
return $key < count($this->hash);
}
/**
* Allows access of the batch using a numerical array index.
*
* @param int $key Index to access.
*
* @return mixed|null
*/
public function offsetGet($key)
{
$i = -1;
foreach ($this->hash as $obj) {
if ($key === ++$i) {
return $this->hash[$obj];
}
}
return null;
}
public function offsetUnset($key)
{
throw new \RuntimeException('Not implemented');
}
public function offsetSet($key, $value)
{
throw new \RuntimeException('Not implemented');
}
}

362
vendor/guzzlehttp/guzzle/src/Client.php vendored Normal file
View File

@ -0,0 +1,362 @@
<?php
namespace GuzzleHttp;
use GuzzleHttp\Event\HasEmitterTrait;
use GuzzleHttp\Message\MessageFactory;
use GuzzleHttp\Message\MessageFactoryInterface;
use GuzzleHttp\Message\RequestInterface;
use GuzzleHttp\Message\FutureResponse;
use GuzzleHttp\Ring\Core;
use GuzzleHttp\Ring\Future\FutureInterface;
use GuzzleHttp\Exception\RequestException;
use React\Promise\FulfilledPromise;
use React\Promise\RejectedPromise;
/**
* HTTP client
*/
class Client implements ClientInterface
{
use HasEmitterTrait;
/** @var MessageFactoryInterface Request factory used by the client */
private $messageFactory;
/** @var Url Base URL of the client */
private $baseUrl;
/** @var array Default request options */
private $defaults;
/** @var callable Request state machine */
private $fsm;
/**
* Clients accept an array of constructor parameters.
*
* Here's an example of creating a client using an URI template for the
* client's base_url and an array of default request options to apply
* to each request:
*
* $client = new Client([
* 'base_url' => [
* 'http://www.foo.com/{version}/',
* ['version' => '123']
* ],
* 'defaults' => [
* 'timeout' => 10,
* 'allow_redirects' => false,
* 'proxy' => '192.168.16.1:10'
* ]
* ]);
*
* @param array $config Client configuration settings
* - base_url: Base URL of the client that is merged into relative URLs.
* Can be a string or an array that contains a URI template followed
* by an associative array of expansion variables to inject into the
* URI template.
* - handler: callable RingPHP handler used to transfer requests
* - message_factory: Factory used to create request and response object
* - defaults: Default request options to apply to each request
* - emitter: Event emitter used for request events
* - fsm: (internal use only) The request finite state machine. A
* function that accepts a transaction and optional final state. The
* function is responsible for transitioning a request through its
* lifecycle events.
*/
public function __construct(array $config = [])
{
$this->configureBaseUrl($config);
$this->configureDefaults($config);
if (isset($config['emitter'])) {
$this->emitter = $config['emitter'];
}
$this->messageFactory = isset($config['message_factory'])
? $config['message_factory']
: new MessageFactory();
if (isset($config['fsm'])) {
$this->fsm = $config['fsm'];
} else {
if (isset($config['handler'])) {
$handler = $config['handler'];
} elseif (isset($config['adapter'])) {
$handler = $config['adapter'];
} else {
$handler = Utils::getDefaultHandler();
}
$this->fsm = new RequestFsm($handler, $this->messageFactory);
}
}
public function getDefaultOption($keyOrPath = null)
{
return $keyOrPath === null
? $this->defaults
: Utils::getPath($this->defaults, $keyOrPath);
}
public function setDefaultOption($keyOrPath, $value)
{
Utils::setPath($this->defaults, $keyOrPath, $value);
}
public function getBaseUrl()
{
return (string) $this->baseUrl;
}
public function createRequest($method, $url = null, array $options = [])
{
$options = $this->mergeDefaults($options);
// Use a clone of the client's emitter
$options['config']['emitter'] = clone $this->getEmitter();
$url = $url || (is_string($url) && strlen($url))
? $this->buildUrl($url)
: (string) $this->baseUrl;
return $this->messageFactory->createRequest($method, $url, $options);
}
public function get($url = null, $options = [])
{
return $this->send($this->createRequest('GET', $url, $options));
}
public function head($url = null, array $options = [])
{
return $this->send($this->createRequest('HEAD', $url, $options));
}
public function delete($url = null, array $options = [])
{
return $this->send($this->createRequest('DELETE', $url, $options));
}
public function put($url = null, array $options = [])
{
return $this->send($this->createRequest('PUT', $url, $options));
}
public function patch($url = null, array $options = [])
{
return $this->send($this->createRequest('PATCH', $url, $options));
}
public function post($url = null, array $options = [])
{
return $this->send($this->createRequest('POST', $url, $options));
}
public function options($url = null, array $options = [])
{
return $this->send($this->createRequest('OPTIONS', $url, $options));
}
public function send(RequestInterface $request)
{
$isFuture = $request->getConfig()->get('future');
$trans = new Transaction($this, $request, $isFuture);
$fn = $this->fsm;
try {
$fn($trans);
if ($isFuture) {
// Turn the normal response into a future if needed.
return $trans->response instanceof FutureInterface
? $trans->response
: new FutureResponse(new FulfilledPromise($trans->response));
}
// Resolve deep futures if this is not a future
// transaction. This accounts for things like retries
// that do not have an immediate side-effect.
while ($trans->response instanceof FutureInterface) {
$trans->response = $trans->response->wait();
}
return $trans->response;
} catch (\Exception $e) {
if ($isFuture) {
// Wrap the exception in a promise
return new FutureResponse(new RejectedPromise($e));
}
throw RequestException::wrapException($trans->request, $e);
} catch (\TypeError $error) {
$exception = new \Exception($error->getMessage(), $error->getCode(), $error);
if ($isFuture) {
// Wrap the exception in a promise
return new FutureResponse(new RejectedPromise($exception));
}
throw RequestException::wrapException($trans->request, $exception);
}
}
/**
* Get an array of default options to apply to the client
*
* @return array
*/
protected function getDefaultOptions()
{
$settings = [
'allow_redirects' => true,
'exceptions' => true,
'decode_content' => true,
'verify' => true
];
// Use the standard Linux HTTP_PROXY and HTTPS_PROXY if set.
// We can only trust the HTTP_PROXY environment variable in a CLI
// process due to the fact that PHP has no reliable mechanism to
// get environment variables that start with "HTTP_".
if (php_sapi_name() == 'cli' && getenv('HTTP_PROXY')) {
$settings['proxy']['http'] = getenv('HTTP_PROXY');
}
if ($proxy = getenv('HTTPS_PROXY')) {
$settings['proxy']['https'] = $proxy;
}
return $settings;
}
/**
* Expand a URI template and inherit from the base URL if it's relative
*
* @param string|array $url URL or an array of the URI template to expand
* followed by a hash of template varnames.
* @return string
* @throws \InvalidArgumentException
*/
private function buildUrl($url)
{
// URI template (absolute or relative)
if (!is_array($url)) {
return strpos($url, '://')
? (string) $url
: (string) $this->baseUrl->combine($url);
}
if (!isset($url[1])) {
throw new \InvalidArgumentException('You must provide a hash of '
. 'varname options in the second element of a URL array.');
}
// Absolute URL
if (strpos($url[0], '://')) {
return Utils::uriTemplate($url[0], $url[1]);
}
// Combine the relative URL with the base URL
return (string) $this->baseUrl->combine(
Utils::uriTemplate($url[0], $url[1])
);
}
private function configureBaseUrl(&$config)
{
if (!isset($config['base_url'])) {
$this->baseUrl = new Url('', '');
} elseif (!is_array($config['base_url'])) {
$this->baseUrl = Url::fromString($config['base_url']);
} elseif (count($config['base_url']) < 2) {
throw new \InvalidArgumentException('You must provide a hash of '
. 'varname options in the second element of a base_url array.');
} else {
$this->baseUrl = Url::fromString(
Utils::uriTemplate(
$config['base_url'][0],
$config['base_url'][1]
)
);
$config['base_url'] = (string) $this->baseUrl;
}
}
private function configureDefaults($config)
{
if (!isset($config['defaults'])) {
$this->defaults = $this->getDefaultOptions();
} else {
$this->defaults = array_replace(
$this->getDefaultOptions(),
$config['defaults']
);
}
// Add the default user-agent header
if (!isset($this->defaults['headers'])) {
$this->defaults['headers'] = [
'User-Agent' => Utils::getDefaultUserAgent()
];
} elseif (!Core::hasHeader($this->defaults, 'User-Agent')) {
// Add the User-Agent header if one was not already set
$this->defaults['headers']['User-Agent'] = Utils::getDefaultUserAgent();
}
}
/**
* Merges default options into the array passed by reference.
*
* @param array $options Options to modify by reference
*
* @return array
*/
private function mergeDefaults($options)
{
$defaults = $this->defaults;
// Case-insensitively merge in default headers if both defaults and
// options have headers specified.
if (!empty($defaults['headers']) && !empty($options['headers'])) {
// Create a set of lowercased keys that are present.
$lkeys = [];
foreach (array_keys($options['headers']) as $k) {
$lkeys[strtolower($k)] = true;
}
// Merge in lowercase default keys when not present in above set.
foreach ($defaults['headers'] as $key => $value) {
if (!isset($lkeys[strtolower($key)])) {
$options['headers'][$key] = $value;
}
}
// No longer need to merge in headers.
unset($defaults['headers']);
}
$result = array_replace_recursive($defaults, $options);
foreach ($options as $k => $v) {
if ($v === null) {
unset($result[$k]);
}
}
return $result;
}
/**
* @deprecated Use {@see GuzzleHttp\Pool} instead.
* @see GuzzleHttp\Pool
*/
public function sendAll($requests, array $options = [])
{
Pool::send($this, $requests, $options);
}
/**
* @deprecated Use GuzzleHttp\Utils::getDefaultHandler
*/
public static function getDefaultHandler()
{
return Utils::getDefaultHandler();
}
/**
* @deprecated Use GuzzleHttp\Utils::getDefaultUserAgent
*/
public static function getDefaultUserAgent()
{
return Utils::getDefaultUserAgent();
}
}

View File

@ -0,0 +1,150 @@
<?php
namespace GuzzleHttp;
use GuzzleHttp\Event\HasEmitterInterface;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Message\RequestInterface;
use GuzzleHttp\Message\ResponseInterface;
/**
* Client interface for sending HTTP requests
*/
interface ClientInterface extends HasEmitterInterface
{
const VERSION = '5.3.1';
/**
* Create and return a new {@see RequestInterface} object.
*
* Use an absolute path to override the base path of the client, or a
* relative path to append to the base path of the client. The URL can
* contain the query string as well. Use an array to provide a URL
* template and additional variables to use in the URL template expansion.
*
* @param string $method HTTP method
* @param string|array|Url $url URL or URI template
* @param array $options Array of request options to apply.
*
* @return RequestInterface
*/
public function createRequest($method, $url = null, array $options = []);
/**
* Send a GET request
*
* @param string|array|Url $url URL or URI template
* @param array $options Array of request options to apply.
*
* @return ResponseInterface
* @throws RequestException When an error is encountered
*/
public function get($url = null, $options = []);
/**
* Send a HEAD request
*
* @param string|array|Url $url URL or URI template
* @param array $options Array of request options to apply.
*
* @return ResponseInterface
* @throws RequestException When an error is encountered
*/
public function head($url = null, array $options = []);
/**
* Send a DELETE request
*
* @param string|array|Url $url URL or URI template
* @param array $options Array of request options to apply.
*
* @return ResponseInterface
* @throws RequestException When an error is encountered
*/
public function delete($url = null, array $options = []);
/**
* Send a PUT request
*
* @param string|array|Url $url URL or URI template
* @param array $options Array of request options to apply.
*
* @return ResponseInterface
* @throws RequestException When an error is encountered
*/
public function put($url = null, array $options = []);
/**
* Send a PATCH request
*
* @param string|array|Url $url URL or URI template
* @param array $options Array of request options to apply.
*
* @return ResponseInterface
* @throws RequestException When an error is encountered
*/
public function patch($url = null, array $options = []);
/**
* Send a POST request
*
* @param string|array|Url $url URL or URI template
* @param array $options Array of request options to apply.
*
* @return ResponseInterface
* @throws RequestException When an error is encountered
*/
public function post($url = null, array $options = []);
/**
* Send an OPTIONS request
*
* @param string|array|Url $url URL or URI template
* @param array $options Array of request options to apply.
*
* @return ResponseInterface
* @throws RequestException When an error is encountered
*/
public function options($url = null, array $options = []);
/**
* Sends a single request
*
* @param RequestInterface $request Request to send
*
* @return \GuzzleHttp\Message\ResponseInterface
* @throws \LogicException When the handler does not populate a response
* @throws RequestException When an error is encountered
*/
public function send(RequestInterface $request);
/**
* Get default request options of the client.
*
* @param string|null $keyOrPath The Path to a particular default request
* option to retrieve or pass null to retrieve all default request
* options. The syntax uses "/" to denote a path through nested PHP
* arrays. For example, "headers/content-type".
*
* @return mixed
*/
public function getDefaultOption($keyOrPath = null);
/**
* Set a default request option on the client so that any request created
* by the client will use the provided default value unless overridden
* explicitly when creating a request.
*
* @param string|null $keyOrPath The Path to a particular configuration
* value to set. The syntax uses a path notation that allows you to
* specify nested configuration values (e.g., 'headers/content-type').
* @param mixed $value Default request option value to set
*/
public function setDefaultOption($keyOrPath, $value);
/**
* Get the base URL of the client.
*
* @return string Returns the base URL if present
*/
public function getBaseUrl();
}

View File

@ -0,0 +1,236 @@
<?php
namespace GuzzleHttp;
/**
* Key value pair collection object
*/
class Collection implements
\ArrayAccess,
\IteratorAggregate,
\Countable,
ToArrayInterface
{
use HasDataTrait;
/**
* @param array $data Associative array of data to set
*/
public function __construct(array $data = [])
{
$this->data = $data;
}
/**
* Create a new collection from an array, validate the keys, and add default
* values where missing
*
* @param array $config Configuration values to apply.
* @param array $defaults Default parameters
* @param array $required Required parameter names
*
* @return self
* @throws \InvalidArgumentException if a parameter is missing
*/
public static function fromConfig(
array $config = [],
array $defaults = [],
array $required = []
) {
$data = $config + $defaults;
if ($missing = array_diff($required, array_keys($data))) {
throw new \InvalidArgumentException(
'Config is missing the following keys: ' .
implode(', ', $missing));
}
return new self($data);
}
/**
* Removes all key value pairs
*/
public function clear()
{
$this->data = [];
}
/**
* Get a specific key value.
*
* @param string $key Key to retrieve.
*
* @return mixed|null Value of the key or NULL
*/
public function get($key)
{
return isset($this->data[$key]) ? $this->data[$key] : null;
}
/**
* Set a key value pair
*
* @param string $key Key to set
* @param mixed $value Value to set
*/
public function set($key, $value)
{
$this->data[$key] = $value;
}
/**
* Add a value to a key. If a key of the same name has already been added,
* the key value will be converted into an array and the new value will be
* pushed to the end of the array.
*
* @param string $key Key to add
* @param mixed $value Value to add to the key
*/
public function add($key, $value)
{
if (!array_key_exists($key, $this->data)) {
$this->data[$key] = $value;
} elseif (is_array($this->data[$key])) {
$this->data[$key][] = $value;
} else {
$this->data[$key] = array($this->data[$key], $value);
}
}
/**
* Remove a specific key value pair
*
* @param string $key A key to remove
*/
public function remove($key)
{
unset($this->data[$key]);
}
/**
* Get all keys in the collection
*
* @return array
*/
public function getKeys()
{
return array_keys($this->data);
}
/**
* Returns whether or not the specified key is present.
*
* @param string $key The key for which to check the existence.
*
* @return bool
*/
public function hasKey($key)
{
return array_key_exists($key, $this->data);
}
/**
* Checks if any keys contains a certain value
*
* @param string $value Value to search for
*
* @return mixed Returns the key if the value was found FALSE if the value
* was not found.
*/
public function hasValue($value)
{
return array_search($value, $this->data, true);
}
/**
* Replace the data of the object with the value of an array
*
* @param array $data Associative array of data
*/
public function replace(array $data)
{
$this->data = $data;
}
/**
* Add and merge in a Collection or array of key value pair data.
*
* @param Collection|array $data Associative array of key value pair data
*/
public function merge($data)
{
foreach ($data as $key => $value) {
$this->add($key, $value);
}
}
/**
* Overwrite key value pairs in this collection with all of the data from
* an array or collection.
*
* @param array|\Traversable $data Values to override over this config
*/
public function overwriteWith($data)
{
if (is_array($data)) {
$this->data = $data + $this->data;
} elseif ($data instanceof Collection) {
$this->data = $data->toArray() + $this->data;
} else {
foreach ($data as $key => $value) {
$this->data[$key] = $value;
}
}
}
/**
* Returns a Collection containing all the elements of the collection after
* applying the callback function to each one.
*
* The callable should accept three arguments:
* - (string) $key
* - (string) $value
* - (array) $context
*
* The callable must return a the altered or unaltered value.
*
* @param callable $closure Map function to apply
* @param array $context Context to pass to the callable
*
* @return Collection
*/
public function map(callable $closure, array $context = [])
{
$collection = new static();
foreach ($this as $key => $value) {
$collection[$key] = $closure($key, $value, $context);
}
return $collection;
}
/**
* Iterates over each key value pair in the collection passing them to the
* callable. If the callable returns true, the current value from input is
* returned into the result Collection.
*
* The callable must accept two arguments:
* - (string) $key
* - (string) $value
*
* @param callable $closure Evaluation function
*
* @return Collection
*/
public function filter(callable $closure)
{
$collection = new static();
foreach ($this->data as $key => $value) {
if ($closure($key, $value)) {
$collection[$key] = $value;
}
}
return $collection;
}
}

View File

@ -0,0 +1,248 @@
<?php
namespace GuzzleHttp\Cookie;
use GuzzleHttp\Message\RequestInterface;
use GuzzleHttp\Message\ResponseInterface;
use GuzzleHttp\ToArrayInterface;
/**
* Cookie jar that stores cookies an an array
*/
class CookieJar implements CookieJarInterface, ToArrayInterface
{
/** @var SetCookie[] Loaded cookie data */
private $cookies = [];
/** @var bool */
private $strictMode;
/**
* @param bool $strictMode Set to true to throw exceptions when invalid
* cookies are added to the cookie jar.
* @param array $cookieArray Array of SetCookie objects or a hash of arrays
* that can be used with the SetCookie constructor
*/
public function __construct($strictMode = false, $cookieArray = [])
{
$this->strictMode = $strictMode;
foreach ($cookieArray as $cookie) {
if (!($cookie instanceof SetCookie)) {
$cookie = new SetCookie($cookie);
}
$this->setCookie($cookie);
}
}
/**
* Create a new Cookie jar from an associative array and domain.
*
* @param array $cookies Cookies to create the jar from
* @param string $domain Domain to set the cookies to
*
* @return self
*/
public static function fromArray(array $cookies, $domain)
{
$cookieJar = new self();
foreach ($cookies as $name => $value) {
$cookieJar->setCookie(new SetCookie([
'Domain' => $domain,
'Name' => $name,
'Value' => $value,
'Discard' => true
]));
}
return $cookieJar;
}
/**
* Quote the cookie value if it is not already quoted and it contains
* problematic characters.
*
* @param string $value Value that may or may not need to be quoted
*
* @return string
*/
public static function getCookieValue($value)
{
if (substr($value, 0, 1) !== '"' &&
substr($value, -1, 1) !== '"' &&
strpbrk($value, ';,')
) {
$value = '"' . $value . '"';
}
return $value;
}
public function toArray()
{
return array_map(function (SetCookie $cookie) {
return $cookie->toArray();
}, $this->getIterator()->getArrayCopy());
}
public function clear($domain = null, $path = null, $name = null)
{
if (!$domain) {
$this->cookies = [];
return;
} elseif (!$path) {
$this->cookies = array_filter(
$this->cookies,
function (SetCookie $cookie) use ($path, $domain) {
return !$cookie->matchesDomain($domain);
}
);
} elseif (!$name) {
$this->cookies = array_filter(
$this->cookies,
function (SetCookie $cookie) use ($path, $domain) {
return !($cookie->matchesPath($path) &&
$cookie->matchesDomain($domain));
}
);
} else {
$this->cookies = array_filter(
$this->cookies,
function (SetCookie $cookie) use ($path, $domain, $name) {
return !($cookie->getName() == $name &&
$cookie->matchesPath($path) &&
$cookie->matchesDomain($domain));
}
);
}
}
public function clearSessionCookies()
{
$this->cookies = array_filter(
$this->cookies,
function (SetCookie $cookie) {
return !$cookie->getDiscard() && $cookie->getExpires();
}
);
}
public function setCookie(SetCookie $cookie)
{
// Only allow cookies with set and valid domain, name, value
$result = $cookie->validate();
if ($result !== true) {
if ($this->strictMode) {
throw new \RuntimeException('Invalid cookie: ' . $result);
} else {
$this->removeCookieIfEmpty($cookie);
return false;
}
}
// Resolve conflicts with previously set cookies
foreach ($this->cookies as $i => $c) {
// Two cookies are identical, when their path, and domain are
// identical.
if ($c->getPath() != $cookie->getPath() ||
$c->getDomain() != $cookie->getDomain() ||
$c->getName() != $cookie->getName()
) {
continue;
}
// The previously set cookie is a discard cookie and this one is
// not so allow the new cookie to be set
if (!$cookie->getDiscard() && $c->getDiscard()) {
unset($this->cookies[$i]);
continue;
}
// If the new cookie's expiration is further into the future, then
// replace the old cookie
if ($cookie->getExpires() > $c->getExpires()) {
unset($this->cookies[$i]);
continue;
}
// If the value has changed, we better change it
if ($cookie->getValue() !== $c->getValue()) {
unset($this->cookies[$i]);
continue;
}
// The cookie exists, so no need to continue
return false;
}
$this->cookies[] = $cookie;
return true;
}
public function count()
{
return count($this->cookies);
}
public function getIterator()
{
return new \ArrayIterator(array_values($this->cookies));
}
public function extractCookies(
RequestInterface $request,
ResponseInterface $response
) {
if ($cookieHeader = $response->getHeaderAsArray('Set-Cookie')) {
foreach ($cookieHeader as $cookie) {
$sc = SetCookie::fromString($cookie);
if (!$sc->getDomain()) {
$sc->setDomain($request->getHost());
}
$this->setCookie($sc);
}
}
}
public function addCookieHeader(RequestInterface $request)
{
$values = [];
$scheme = $request->getScheme();
$host = $request->getHost();
$path = $request->getPath();
foreach ($this->cookies as $cookie) {
if ($cookie->matchesPath($path) &&
$cookie->matchesDomain($host) &&
!$cookie->isExpired() &&
(!$cookie->getSecure() || $scheme == 'https')
) {
$values[] = $cookie->getName() . '='
. self::getCookieValue($cookie->getValue());
}
}
if ($values) {
$request->setHeader('Cookie', implode('; ', $values));
}
}
/**
* If a cookie already exists and the server asks to set it again with a
* null value, the cookie must be deleted.
*
* @param SetCookie $cookie
*/
private function removeCookieIfEmpty(SetCookie $cookie)
{
$cookieValue = $cookie->getValue();
if ($cookieValue === null || $cookieValue === '') {
$this->clear(
$cookie->getDomain(),
$cookie->getPath(),
$cookie->getName()
);
}
}
}

View File

@ -0,0 +1,75 @@
<?php
namespace GuzzleHttp\Cookie;
use GuzzleHttp\Message\RequestInterface;
use GuzzleHttp\Message\ResponseInterface;
/**
* Stores HTTP cookies.
*
* It extracts cookies from HTTP requests, and returns them in HTTP responses.
* CookieJarInterface instances automatically expire contained cookies when
* necessary. Subclasses are also responsible for storing and retrieving
* cookies from a file, database, etc.
*
* @link http://docs.python.org/2/library/cookielib.html Inspiration
*/
interface CookieJarInterface extends \Countable, \IteratorAggregate
{
/**
* Add a Cookie header to a request.
*
* If no matching cookies are found in the cookie jar, then no Cookie
* header is added to the request.
*
* @param RequestInterface $request Request object to update
*/
public function addCookieHeader(RequestInterface $request);
/**
* Extract cookies from an HTTP response and store them in the CookieJar.
*
* @param RequestInterface $request Request that was sent
* @param ResponseInterface $response Response that was received
*/
public function extractCookies(
RequestInterface $request,
ResponseInterface $response
);
/**
* Sets a cookie in the cookie jar.
*
* @param SetCookie $cookie Cookie to set.
*
* @return bool Returns true on success or false on failure
*/
public function setCookie(SetCookie $cookie);
/**
* Remove cookies currently held in the cookie jar.
*
* Invoking this method without arguments will empty the whole cookie jar.
* If given a $domain argument only cookies belonging to that domain will
* be removed. If given a $domain and $path argument, cookies belonging to
* the specified path within that domain are removed. If given all three
* arguments, then the cookie with the specified name, path and domain is
* removed.
*
* @param string $domain Clears cookies matching a domain
* @param string $path Clears cookies matching a domain and path
* @param string $name Clears cookies matching a domain, path, and name
*
* @return CookieJarInterface
*/
public function clear($domain = null, $path = null, $name = null);
/**
* Discard all sessions cookies.
*
* Removes cookies that don't have an expire field or a have a discard
* field set to true. To be called when the user agent shuts down according
* to RFC 2965.
*/
public function clearSessionCookies();
}

View File

@ -0,0 +1,86 @@
<?php
namespace GuzzleHttp\Cookie;
use GuzzleHttp\Utils;
/**
* Persists non-session cookies using a JSON formatted file
*/
class FileCookieJar extends CookieJar
{
/** @var string filename */
private $filename;
/**
* Create a new FileCookieJar object
*
* @param string $cookieFile File to store the cookie data
*
* @throws \RuntimeException if the file cannot be found or created
*/
public function __construct($cookieFile)
{
$this->filename = $cookieFile;
if (file_exists($cookieFile)) {
$this->load($cookieFile);
}
}
/**
* Saves the file when shutting down
*/
public function __destruct()
{
$this->save($this->filename);
}
/**
* Saves the cookies to a file.
*
* @param string $filename File to save
* @throws \RuntimeException if the file cannot be found or created
*/
public function save($filename)
{
$json = [];
foreach ($this as $cookie) {
if ($cookie->getExpires() && !$cookie->getDiscard()) {
$json[] = $cookie->toArray();
}
}
if (false === file_put_contents($filename, json_encode($json))) {
// @codeCoverageIgnoreStart
throw new \RuntimeException("Unable to save file {$filename}");
// @codeCoverageIgnoreEnd
}
}
/**
* Load cookies from a JSON formatted file.
*
* Old cookies are kept unless overwritten by newly loaded ones.
*
* @param string $filename Cookie file to load.
* @throws \RuntimeException if the file cannot be loaded.
*/
public function load($filename)
{
$json = file_get_contents($filename);
if (false === $json) {
// @codeCoverageIgnoreStart
throw new \RuntimeException("Unable to load file {$filename}");
// @codeCoverageIgnoreEnd
}
$data = Utils::jsonDecode($json, true);
if (is_array($data)) {
foreach (Utils::jsonDecode($json, true) as $cookie) {
$this->setCookie(new SetCookie($cookie));
}
} elseif (strlen($data)) {
throw new \RuntimeException("Invalid cookie file: {$filename}");
}
}
}

View File

@ -0,0 +1,66 @@
<?php
namespace GuzzleHttp\Cookie;
use GuzzleHttp\Utils;
/**
* Persists cookies in the client session
*/
class SessionCookieJar extends CookieJar
{
/** @var string session key */
private $sessionKey;
/**
* Create a new SessionCookieJar object
*
* @param string $sessionKey Session key name to store the cookie data in session
*/
public function __construct($sessionKey)
{
$this->sessionKey = $sessionKey;
$this->load();
}
/**
* Saves cookies to session when shutting down
*/
public function __destruct()
{
$this->save();
}
/**
* Save cookies to the client session
*/
public function save()
{
$json = [];
foreach ($this as $cookie) {
if ($cookie->getExpires() && !$cookie->getDiscard()) {
$json[] = $cookie->toArray();
}
}
$_SESSION[$this->sessionKey] = json_encode($json);
}
/**
* Load the contents of the client session into the data array
*/
protected function load()
{
$cookieJar = isset($_SESSION[$this->sessionKey])
? $_SESSION[$this->sessionKey]
: null;
$data = Utils::jsonDecode($cookieJar, true);
if (is_array($data)) {
foreach ($data as $cookie) {
$this->setCookie(new SetCookie($cookie));
}
} elseif (strlen($data)) {
throw new \RuntimeException("Invalid cookie data");
}
}
}

View File

@ -0,0 +1,373 @@
<?php
namespace GuzzleHttp\Cookie;
use GuzzleHttp\ToArrayInterface;
/**
* Set-Cookie object
*/
class SetCookie implements ToArrayInterface
{
/** @var array */
private static $defaults = [
'Name' => null,
'Value' => null,
'Domain' => null,
'Path' => '/',
'Max-Age' => null,
'Expires' => null,
'Secure' => false,
'Discard' => false,
'HttpOnly' => false
];
/** @var array Cookie data */
private $data;
/**
* Create a new SetCookie object from a string
*
* @param string $cookie Set-Cookie header string
*
* @return self
*/
public static function fromString($cookie)
{
// Create the default return array
$data = self::$defaults;
// Explode the cookie string using a series of semicolons
$pieces = array_filter(array_map('trim', explode(';', $cookie)));
// The name of the cookie (first kvp) must include an equal sign.
if (empty($pieces) || !strpos($pieces[0], '=')) {
return new self($data);
}
// Add the cookie pieces into the parsed data array
foreach ($pieces as $part) {
$cookieParts = explode('=', $part, 2);
$key = trim($cookieParts[0]);
$value = isset($cookieParts[1])
? trim($cookieParts[1], " \n\r\t\0\x0B\"")
: true;
// Only check for non-cookies when cookies have been found
if (empty($data['Name'])) {
$data['Name'] = $key;
$data['Value'] = $value;
} else {
foreach (array_keys(self::$defaults) as $search) {
if (!strcasecmp($search, $key)) {
$data[$search] = $value;
continue 2;
}
}
$data[$key] = $value;
}
}
return new self($data);
}
/**
* @param array $data Array of cookie data provided by a Cookie parser
*/
public function __construct(array $data = [])
{
$this->data = array_replace(self::$defaults, $data);
// Extract the Expires value and turn it into a UNIX timestamp if needed
if (!$this->getExpires() && $this->getMaxAge()) {
// Calculate the Expires date
$this->setExpires(time() + $this->getMaxAge());
} elseif ($this->getExpires() && !is_numeric($this->getExpires())) {
$this->setExpires($this->getExpires());
}
}
public function __toString()
{
$str = $this->data['Name'] . '=' . $this->data['Value'] . '; ';
foreach ($this->data as $k => $v) {
if ($k != 'Name' && $k != 'Value' && $v !== null && $v !== false) {
if ($k == 'Expires') {
$str .= 'Expires=' . gmdate('D, d M Y H:i:s \G\M\T', $v) . '; ';
} else {
$str .= ($v === true ? $k : "{$k}={$v}") . '; ';
}
}
}
return rtrim($str, '; ');
}
public function toArray()
{
return $this->data;
}
/**
* Get the cookie name
*
* @return string
*/
public function getName()
{
return $this->data['Name'];
}
/**
* Set the cookie name
*
* @param string $name Cookie name
*/
public function setName($name)
{
$this->data['Name'] = $name;
}
/**
* Get the cookie value
*
* @return string
*/
public function getValue()
{
return $this->data['Value'];
}
/**
* Set the cookie value
*
* @param string $value Cookie value
*/
public function setValue($value)
{
$this->data['Value'] = $value;
}
/**
* Get the domain
*
* @return string|null
*/
public function getDomain()
{
return $this->data['Domain'];
}
/**
* Set the domain of the cookie
*
* @param string $domain
*/
public function setDomain($domain)
{
$this->data['Domain'] = $domain;
}
/**
* Get the path
*
* @return string
*/
public function getPath()
{
return $this->data['Path'];
}
/**
* Set the path of the cookie
*
* @param string $path Path of the cookie
*/
public function setPath($path)
{
$this->data['Path'] = $path;
}
/**
* Maximum lifetime of the cookie in seconds
*
* @return int|null
*/
public function getMaxAge()
{
return $this->data['Max-Age'];
}
/**
* Set the max-age of the cookie
*
* @param int $maxAge Max age of the cookie in seconds
*/
public function setMaxAge($maxAge)
{
$this->data['Max-Age'] = $maxAge;
}
/**
* The UNIX timestamp when the cookie Expires
*
* @return mixed
*/
public function getExpires()
{
return $this->data['Expires'];
}
/**
* Set the unix timestamp for which the cookie will expire
*
* @param int $timestamp Unix timestamp
*/
public function setExpires($timestamp)
{
$this->data['Expires'] = is_numeric($timestamp)
? (int) $timestamp
: strtotime($timestamp);
}
/**
* Get whether or not this is a secure cookie
*
* @return null|bool
*/
public function getSecure()
{
return $this->data['Secure'];
}
/**
* Set whether or not the cookie is secure
*
* @param bool $secure Set to true or false if secure
*/
public function setSecure($secure)
{
$this->data['Secure'] = $secure;
}
/**
* Get whether or not this is a session cookie
*
* @return null|bool
*/
public function getDiscard()
{
return $this->data['Discard'];
}
/**
* Set whether or not this is a session cookie
*
* @param bool $discard Set to true or false if this is a session cookie
*/
public function setDiscard($discard)
{
$this->data['Discard'] = $discard;
}
/**
* Get whether or not this is an HTTP only cookie
*
* @return bool
*/
public function getHttpOnly()
{
return $this->data['HttpOnly'];
}
/**
* Set whether or not this is an HTTP only cookie
*
* @param bool $httpOnly Set to true or false if this is HTTP only
*/
public function setHttpOnly($httpOnly)
{
$this->data['HttpOnly'] = $httpOnly;
}
/**
* Check if the cookie matches a path value
*
* @param string $path Path to check against
*
* @return bool
*/
public function matchesPath($path)
{
return !$this->getPath() || 0 === stripos($path, $this->getPath());
}
/**
* Check if the cookie matches a domain value
*
* @param string $domain Domain to check against
*
* @return bool
*/
public function matchesDomain($domain)
{
// Remove the leading '.' as per spec in RFC 6265.
// http://tools.ietf.org/html/rfc6265#section-5.2.3
$cookieDomain = ltrim($this->getDomain(), '.');
// Domain not set or exact match.
if (!$cookieDomain || !strcasecmp($domain, $cookieDomain)) {
return true;
}
// Matching the subdomain according to RFC 6265.
// http://tools.ietf.org/html/rfc6265#section-5.1.3
if (filter_var($domain, FILTER_VALIDATE_IP)) {
return false;
}
return (bool) preg_match('/\.' . preg_quote($cookieDomain) . '$/i', $domain);
}
/**
* Check if the cookie is expired
*
* @return bool
*/
public function isExpired()
{
return $this->getExpires() && time() > $this->getExpires();
}
/**
* Check if the cookie is valid according to RFC 6265
*
* @return bool|string Returns true if valid or an error message if invalid
*/
public function validate()
{
// Names must not be empty, but can be 0
$name = $this->getName();
if (empty($name) && !is_numeric($name)) {
return 'The cookie name must not be empty';
}
// Check if any of the invalid characters are present in the cookie name
if (preg_match("/[=,; \t\r\n\013\014]/", $name)) {
return "Cookie name must not cannot invalid characters: =,; \\t\\r\\n\\013\\014";
}
// Value must not be empty, but can be 0
$value = $this->getValue();
if (empty($value) && !is_numeric($value)) {
return 'The cookie value must not be empty';
}
// Domains must not be empty, but can be 0
// A "0" is not a valid internet domain, but may be used as server name
// in a private network.
$domain = $this->getDomain();
if (empty($domain) && !is_numeric($domain)) {
return 'The cookie domain must not be empty';
}
return true;
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace GuzzleHttp\Event;
/**
* Basic event class that can be extended.
*/
abstract class AbstractEvent implements EventInterface
{
private $propagationStopped = false;
public function isPropagationStopped()
{
return $this->propagationStopped;
}
public function stopPropagation()
{
$this->propagationStopped = true;
}
}

View File

@ -0,0 +1,61 @@
<?php
namespace GuzzleHttp\Event;
use GuzzleHttp\Transaction;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Message\RequestInterface;
/**
* Base class for request events, providing a request and client getter.
*/
abstract class AbstractRequestEvent extends AbstractEvent
{
/** @var Transaction */
protected $transaction;
/**
* @param Transaction $transaction
*/
public function __construct(Transaction $transaction)
{
$this->transaction = $transaction;
}
/**
* Get the HTTP client associated with the event.
*
* @return ClientInterface
*/
public function getClient()
{
return $this->transaction->client;
}
/**
* Get the request object
*
* @return RequestInterface
*/
public function getRequest()
{
return $this->transaction->request;
}
/**
* Get the number of transaction retries.
*
* @return int
*/
public function getRetryCount()
{
return $this->transaction->retries;
}
/**
* @return Transaction
*/
public function getTransaction()
{
return $this->transaction;
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace GuzzleHttp\Event;
/**
* Abstract request event that can be retried.
*/
class AbstractRetryableEvent extends AbstractTransferEvent
{
/**
* Mark the request as needing a retry and stop event propagation.
*
* This action allows you to retry a request without emitting the "end"
* event multiple times for a given request. When retried, the request
* emits a before event and is then sent again using the client that sent
* the original request.
*
* When retrying, it is important to limit the number of retries you allow
* to prevent infinite loops.
*
* This action can only be taken during the "complete" and "error" events.
*
* @param int $afterDelay If specified, the amount of time in milliseconds
* to delay before retrying. Note that this must
* be supported by the underlying RingPHP handler
* to work properly. Set to 0 or provide no value
* to retry immediately.
*/
public function retry($afterDelay = 0)
{
// Setting the transition state to 'retry' will cause the next state
// transition of the transaction to retry the request.
$this->transaction->state = 'retry';
if ($afterDelay) {
$this->transaction->request->getConfig()->set('delay', $afterDelay);
}
$this->stopPropagation();
}
}

View File

@ -0,0 +1,63 @@
<?php
namespace GuzzleHttp\Event;
use GuzzleHttp\Message\ResponseInterface;
use GuzzleHttp\Ring\Future\FutureInterface;
/**
* Event that contains transfer statistics, and can be intercepted.
*/
abstract class AbstractTransferEvent extends AbstractRequestEvent
{
/**
* Get all transfer information as an associative array if no $name
* argument is supplied, or gets a specific transfer statistic if
* a $name attribute is supplied (e.g., 'total_time').
*
* @param string $name Name of the transfer stat to retrieve
*
* @return mixed|null|array
*/
public function getTransferInfo($name = null)
{
if (!$name) {
return $this->transaction->transferInfo;
}
return isset($this->transaction->transferInfo[$name])
? $this->transaction->transferInfo[$name]
: null;
}
/**
* Returns true/false if a response is available.
*
* @return bool
*/
public function hasResponse()
{
return !($this->transaction->response instanceof FutureInterface);
}
/**
* Get the response.
*
* @return ResponseInterface|null
*/
public function getResponse()
{
return $this->hasResponse() ? $this->transaction->response : null;
}
/**
* Intercept the request and associate a response
*
* @param ResponseInterface $response Response to set
*/
public function intercept(ResponseInterface $response)
{
$this->transaction->response = $response;
$this->transaction->exception = null;
$this->stopPropagation();
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace GuzzleHttp\Event;
use GuzzleHttp\Message\ResponseInterface;
/**
* Event object emitted before a request is sent.
*
* This event MAY be emitted multiple times (i.e., if a request is retried).
* You MAY change the Response associated with the request using the
* intercept() method of the event.
*/
class BeforeEvent extends AbstractRequestEvent
{
/**
* Intercept the request and associate a response
*
* @param ResponseInterface $response Response to set
*/
public function intercept(ResponseInterface $response)
{
$this->transaction->response = $response;
$this->transaction->exception = null;
$this->stopPropagation();
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace GuzzleHttp\Event;
/**
* Event object emitted after a request has been completed.
*
* This event MAY be emitted multiple times for a single request. You MAY
* change the Response associated with the request using the intercept()
* method of the event.
*
* This event allows the request to be retried if necessary using the retry()
* method of the event.
*/
class CompleteEvent extends AbstractRetryableEvent {}

View File

@ -0,0 +1,145 @@
<?php
namespace GuzzleHttp\Event;
/**
* Guzzle event emitter.
*
* Some of this class is based on the Symfony EventDispatcher component, which
* ships with the following license:
*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link https://github.com/symfony/symfony/tree/master/src/Symfony/Component/EventDispatcher
*/
class Emitter implements EmitterInterface
{
/** @var array */
private $listeners = [];
/** @var array */
private $sorted = [];
public function on($eventName, callable $listener, $priority = 0)
{
if ($priority === 'first') {
$priority = isset($this->listeners[$eventName])
? max(array_keys($this->listeners[$eventName])) + 1
: 1;
} elseif ($priority === 'last') {
$priority = isset($this->listeners[$eventName])
? min(array_keys($this->listeners[$eventName])) - 1
: -1;
}
$this->listeners[$eventName][$priority][] = $listener;
unset($this->sorted[$eventName]);
}
public function once($eventName, callable $listener, $priority = 0)
{
$onceListener = function (
EventInterface $event
) use (&$onceListener, $eventName, $listener, $priority) {
$this->removeListener($eventName, $onceListener);
$listener($event, $eventName);
};
$this->on($eventName, $onceListener, $priority);
}
public function removeListener($eventName, callable $listener)
{
if (empty($this->listeners[$eventName])) {
return;
}
foreach ($this->listeners[$eventName] as $priority => $listeners) {
if (false !== ($key = array_search($listener, $listeners, true))) {
unset(
$this->listeners[$eventName][$priority][$key],
$this->sorted[$eventName]
);
}
}
}
public function listeners($eventName = null)
{
// Return all events in a sorted priority order
if ($eventName === null) {
foreach (array_keys($this->listeners) as $eventName) {
if (empty($this->sorted[$eventName])) {
$this->listeners($eventName);
}
}
return $this->sorted;
}
// Return the listeners for a specific event, sorted in priority order
if (empty($this->sorted[$eventName])) {
$this->sorted[$eventName] = [];
if (isset($this->listeners[$eventName])) {
krsort($this->listeners[$eventName], SORT_NUMERIC);
foreach ($this->listeners[$eventName] as $listeners) {
foreach ($listeners as $listener) {
$this->sorted[$eventName][] = $listener;
}
}
}
}
return $this->sorted[$eventName];
}
public function hasListeners($eventName)
{
return !empty($this->listeners[$eventName]);
}
public function emit($eventName, EventInterface $event)
{
if (isset($this->listeners[$eventName])) {
foreach ($this->listeners($eventName) as $listener) {
$listener($event, $eventName);
if ($event->isPropagationStopped()) {
break;
}
}
}
return $event;
}
public function attach(SubscriberInterface $subscriber)
{
foreach ($subscriber->getEvents() as $eventName => $listeners) {
if (is_array($listeners[0])) {
foreach ($listeners as $listener) {
$this->on(
$eventName,
[$subscriber, $listener[0]],
isset($listener[1]) ? $listener[1] : 0
);
}
} else {
$this->on(
$eventName,
[$subscriber, $listeners[0]],
isset($listeners[1]) ? $listeners[1] : 0
);
}
}
}
public function detach(SubscriberInterface $subscriber)
{
foreach ($subscriber->getEvents() as $eventName => $listener) {
$this->removeListener($eventName, [$subscriber, $listener[0]]);
}
}
}

View File

@ -0,0 +1,96 @@
<?php
namespace GuzzleHttp\Event;
/**
* Guzzle event emitter.
*/
interface EmitterInterface
{
/**
* Binds a listener to a specific event.
*
* @param string $eventName Name of the event to bind to.
* @param callable $listener Listener to invoke when triggered.
* @param int|string $priority The higher this value, the earlier an event
* listener will be triggered in the chain (defaults to 0). You can
* pass "first" or "last" to dynamically specify the event priority
* based on the current event priorities associated with the given
* event name in the emitter. Use "first" to set the priority to the
* current highest priority plus one. Use "last" to set the priority to
* the current lowest event priority minus one.
*/
public function on($eventName, callable $listener, $priority = 0);
/**
* Binds a listener to a specific event. After the listener is triggered
* once, it is removed as a listener.
*
* @param string $eventName Name of the event to bind to.
* @param callable $listener Listener to invoke when triggered.
* @param int $priority The higher this value, the earlier an event
* listener will be triggered in the chain (defaults to 0)
*/
public function once($eventName, callable $listener, $priority = 0);
/**
* Removes an event listener from the specified event.
*
* @param string $eventName The event to remove a listener from
* @param callable $listener The listener to remove
*/
public function removeListener($eventName, callable $listener);
/**
* Gets the listeners of a specific event or all listeners if no event is
* specified.
*
* @param string $eventName The name of the event. Pass null (the default)
* to retrieve all listeners.
*
* @return array The event listeners for the specified event, or all event
* listeners by event name. The format of the array when retrieving a
* specific event list is an array of callables. The format of the array
* when retrieving all listeners is an associative array of arrays of
* callables.
*/
public function listeners($eventName = null);
/**
* Checks if the emitter has listeners by the given name.
*
* @param string $eventName The name of the event to check.
*
* @return bool
*/
public function hasListeners($eventName);
/**
* Emits an event to all registered listeners.
*
* Each event that is bound to the emitted eventName receives a
* EventInterface, the name of the event, and the event emitter.
*
* @param string $eventName The name of the event to dispatch.
* @param EventInterface $event The event to pass to the event handlers/listeners.
*
* @return EventInterface Returns the provided event object
*/
public function emit($eventName, EventInterface $event);
/**
* Attaches an event subscriber.
*
* The subscriber is asked for all the events it is interested in and added
* as an event listener for each event.
*
* @param SubscriberInterface $subscriber Subscriber to attach.
*/
public function attach(SubscriberInterface $subscriber);
/**
* Detaches an event subscriber.
*
* @param SubscriberInterface $subscriber Subscriber to detach.
*/
public function detach(SubscriberInterface $subscriber);
}

View File

@ -0,0 +1,28 @@
<?php
namespace GuzzleHttp\Event;
/**
* A terminal event that is emitted when a request transaction has ended.
*
* This event is emitted for both successful responses and responses that
* encountered an exception. You need to check if an exception is present
* in your listener to know the difference.
*
* You MAY intercept the response associated with the event if needed, but keep
* in mind that the "complete" event will not be triggered as a result.
*/
class EndEvent extends AbstractTransferEvent
{
/**
* Get the exception that was encountered (if any).
*
* This method should be used to check if the request was sent successfully
* or if it encountered errors.
*
* @return \Exception|null
*/
public function getException()
{
return $this->transaction->exception;
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace GuzzleHttp\Event;
use GuzzleHttp\Exception\RequestException;
/**
* Event emitted when an error occurs while sending a request.
*
* This event MAY be emitted multiple times. You MAY intercept the exception
* and inject a response into the event to rescue the request using the
* intercept() method of the event.
*
* This event allows the request to be retried using the "retry" method of the
* event.
*/
class ErrorEvent extends AbstractRetryableEvent
{
/**
* Get the exception that was encountered
*
* @return RequestException
*/
public function getException()
{
return $this->transaction->exception;
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace GuzzleHttp\Event;
/**
* Base event interface used when dispatching events to listeners using an
* event emitter.
*/
interface EventInterface
{
/**
* Returns whether or not stopPropagation was called on the event.
*
* @return bool
* @see Event::stopPropagation
*/
public function isPropagationStopped();
/**
* Stops the propagation of the event, preventing subsequent listeners
* registered to the same event from being invoked.
*/
public function stopPropagation();
}

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