forked from GNUsocial/gnu-social
730 lines
20 KiB
PHP
Executable File
730 lines
20 KiB
PHP
Executable File
<?php
|
|
/**
|
|
* Phergie
|
|
*
|
|
* PHP version 5
|
|
*
|
|
* LICENSE
|
|
*
|
|
* This source file is subject to the new BSD license that is bundled
|
|
* with this package in the file LICENSE.
|
|
* It is also available through the world-wide-web at this URL:
|
|
* http://phergie.org/license
|
|
*
|
|
* @category Phergie
|
|
* @package Phergie
|
|
* @author Phergie Development Team <team@phergie.org>
|
|
* @copyright 2008-2010 Phergie Development Team (http://phergie.org)
|
|
* @license http://phergie.org/license New BSD License
|
|
* @link http://pear.phergie.org/package/Phergie
|
|
*/
|
|
|
|
/**
|
|
* Driver that uses the sockets wrapper of the streams extension for
|
|
* communicating with the server and handles formatting and parsing of
|
|
* events using PHP.
|
|
*
|
|
* @category Phergie
|
|
* @package Phergie
|
|
* @author Phergie Development Team <team@phergie.org>
|
|
* @license http://phergie.org/license New BSD License
|
|
* @link http://pear.phergie.org/package/Phergie
|
|
*/
|
|
class Phergie_Driver_Streams extends Phergie_Driver_Abstract
|
|
{
|
|
/**
|
|
* Socket handlers
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $sockets = array();
|
|
|
|
/**
|
|
* Reference to the currently active socket handler
|
|
*
|
|
* @var resource
|
|
*/
|
|
protected $socket;
|
|
|
|
/**
|
|
* Amount of time in seconds to wait to receive an event each time the
|
|
* socket is polled
|
|
*
|
|
* @var float
|
|
*/
|
|
protected $timeout = 0.1;
|
|
|
|
/**
|
|
* Handles construction of command strings and their transmission to the
|
|
* server.
|
|
*
|
|
* @param string $command Command to send
|
|
* @param string|array $args Optional string or array of sequential
|
|
* arguments
|
|
*
|
|
* @return string Command string that was sent
|
|
* @throws Phergie_Driver_Exception
|
|
*/
|
|
protected function send($command, $args = '')
|
|
{
|
|
$connection = $this->getConnection();
|
|
$encoding = $connection->getEncoding();
|
|
|
|
// Require an open socket connection to continue
|
|
if (empty($this->socket)) {
|
|
throw new Phergie_Driver_Exception(
|
|
'doConnect() must be called first',
|
|
Phergie_Driver_Exception::ERR_NO_INITIATED_CONNECTION
|
|
);
|
|
}
|
|
|
|
// Add the command
|
|
$buffer = strtoupper($command);
|
|
|
|
// Add arguments
|
|
if (!empty($args)) {
|
|
|
|
// Apply formatting if arguments are passed in as an array
|
|
if (is_array($args)) {
|
|
$end = count($args) - 1;
|
|
$args[$end] = ':' . $args[$end];
|
|
$args = implode(' ', $args);
|
|
} else {
|
|
$args = ':' . $args;
|
|
}
|
|
|
|
$buffer .= ' ' . $args;
|
|
}
|
|
|
|
// Transmit the command over the socket connection
|
|
$attempts = $written = 0;
|
|
$temp = $buffer . "\r\n";
|
|
$is_multibyte = !substr($encoding, 0, 8) === 'ISO-8859' && $encoding !== 'ASCII' && $encoding !== 'CP1252';
|
|
$length = ($is_multibyte) ? mb_strlen($buffer, '8bit') : strlen($buffer);
|
|
while (true) {
|
|
$written += (int) fwrite($this->socket, $temp);
|
|
if ($written < $length) {
|
|
$temp = substr($temp, $written);
|
|
$attempts++;
|
|
if ($attempts == 3) {
|
|
throw new Phergie_Driver_Exception(
|
|
'Unable to write to socket',
|
|
Phergie_Driver_Exception::ERR_CONNECTION_WRITE_FAILED
|
|
);
|
|
}
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Return the command string that was transmitted
|
|
return $buffer;
|
|
}
|
|
|
|
/**
|
|
* Overrides the parent class to set the currently active socket handler
|
|
* when the active connection is changed.
|
|
*
|
|
* @param Phergie_Connection $connection Active connection
|
|
*
|
|
* @return Phergie_Driver_Streams Provides a fluent interface
|
|
*/
|
|
public function setConnection(Phergie_Connection $connection)
|
|
{
|
|
// Set the active socket handler
|
|
$hostmask = (string) $connection->getHostmask();
|
|
if (!empty($this->sockets[$hostmask])) {
|
|
$this->socket = $this->sockets[$hostmask];
|
|
}
|
|
|
|
// Set the active connection
|
|
return parent::setConnection($connection);
|
|
}
|
|
|
|
/**
|
|
* Returns a list of hostmasks corresponding to sockets with data to read.
|
|
*
|
|
* @param int $sec Length of time to wait for new data (seconds)
|
|
* @param int $usec Length of time to wait for new data (microseconds)
|
|
*
|
|
* @return array List of hostmasks or an empty array if none were found
|
|
* to have data to read
|
|
*/
|
|
public function getActiveReadSockets($sec = 0, $usec = 200000)
|
|
{
|
|
$read = $this->sockets;
|
|
$write = null;
|
|
$error = null;
|
|
$active = array();
|
|
|
|
if (count($this->sockets) > 0) {
|
|
$number = stream_select($read, $write, $error, $sec, $usec);
|
|
if ($number > 0) {
|
|
foreach ($read as $item) {
|
|
$active[] = array_search($item, $this->sockets);
|
|
}
|
|
}
|
|
}
|
|
|
|
return $active;
|
|
}
|
|
|
|
/**
|
|
* Sets the amount of time to wait for a new event each time the socket
|
|
* is polled.
|
|
*
|
|
* @param float $timeout Amount of time in seconds
|
|
*
|
|
* @return Phergie_Driver_Streams Provides a fluent interface
|
|
*/
|
|
public function setTimeout($timeout)
|
|
{
|
|
$timeout = (float) $timeout;
|
|
if ($timeout) {
|
|
$this->timeout = $timeout;
|
|
}
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Returns the amount of time to wait for a new event each time the
|
|
* socket is polled.
|
|
*
|
|
* @return float Amount of time in seconds
|
|
*/
|
|
public function getTimeout()
|
|
{
|
|
return $this->timeout;
|
|
}
|
|
|
|
/**
|
|
* Supporting method to parse event argument strings where the last
|
|
* argument may contain a colon.
|
|
*
|
|
* @param string $args Argument string to parse
|
|
* @param int $count Optional maximum number of arguments
|
|
*
|
|
* @return array Array of argument values
|
|
*/
|
|
protected function parseArguments($args, $count = -1)
|
|
{
|
|
return preg_split('/ :?/S', $args, $count);
|
|
}
|
|
|
|
/**
|
|
* Listens for an event on the current connection.
|
|
*
|
|
* @return Phergie_Event_Interface|null Event instance if an event was
|
|
* received, NULL otherwise
|
|
*/
|
|
public function getEvent()
|
|
{
|
|
// Check the socket is still active
|
|
if (feof($this->socket)) {
|
|
throw new Phergie_Driver_Exception(
|
|
'EOF detected on socket',
|
|
Phergie_Driver_Exception::ERR_CONNECTION_READ_FAILED
|
|
);
|
|
}
|
|
|
|
// Check for a new event on the current connection
|
|
$buffer = fgets($this->socket, 512);
|
|
|
|
// If no new event was found, return NULL
|
|
if (empty($buffer)) {
|
|
return null;
|
|
}
|
|
|
|
// Strip the trailing newline from the buffer
|
|
$buffer = rtrim($buffer);
|
|
|
|
// If the event is from the server...
|
|
if (substr($buffer, 0, 1) != ':') {
|
|
|
|
// Parse the command and arguments
|
|
list($cmd, $args) = array_pad(explode(' ', $buffer, 2), 2, null);
|
|
$hostmask = new Phergie_Hostmask(null, null, $this->connection->getHost());
|
|
|
|
} else {
|
|
// If the event could be from the server or a user...
|
|
|
|
// Parse the server hostname or user hostmask, command, and arguments
|
|
list($prefix, $cmd, $args)
|
|
= array_pad(explode(' ', ltrim($buffer, ':'), 3), 3, null);
|
|
if (strpos($prefix, '@') !== false) {
|
|
$hostmask = Phergie_Hostmask::fromString($prefix);
|
|
} else {
|
|
$hostmask = new Phergie_Hostmask(null, null, $prefix);
|
|
}
|
|
}
|
|
|
|
// Parse the event arguments depending on the event type
|
|
$cmd = strtolower($cmd);
|
|
switch ($cmd) {
|
|
case 'names':
|
|
case 'nick':
|
|
case 'quit':
|
|
case 'ping':
|
|
case 'join':
|
|
case 'error':
|
|
$args = array(ltrim($args, ':'));
|
|
break;
|
|
|
|
case 'privmsg':
|
|
case 'notice':
|
|
$args = $this->parseArguments($args, 2);
|
|
list($source, $ctcp) = $args;
|
|
if (substr($ctcp, 0, 1) === "\001" && substr($ctcp, -1) === "\001") {
|
|
$ctcp = substr($ctcp, 1, -1);
|
|
$reply = ($cmd == 'notice');
|
|
list($cmd, $args) = array_pad(explode(' ', $ctcp, 2), 2, null);
|
|
$cmd = strtolower($cmd);
|
|
switch ($cmd) {
|
|
case 'version':
|
|
case 'time':
|
|
case 'finger':
|
|
if ($reply) {
|
|
$args = $ctcp;
|
|
}
|
|
break;
|
|
case 'ping':
|
|
if ($reply) {
|
|
$cmd .= 'Response';
|
|
} else {
|
|
$cmd = 'ctcpPing';
|
|
}
|
|
break;
|
|
case 'action':
|
|
$args = array($source, $args);
|
|
break;
|
|
|
|
default:
|
|
$cmd = 'ctcp';
|
|
if ($reply) {
|
|
$cmd .= 'Response';
|
|
}
|
|
$args = array($source, $args);
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'oper':
|
|
case 'topic':
|
|
case 'mode':
|
|
$args = $this->parseArguments($args);
|
|
break;
|
|
|
|
case 'part':
|
|
case 'kill':
|
|
case 'invite':
|
|
$args = $this->parseArguments($args, 2);
|
|
break;
|
|
|
|
case 'kick':
|
|
$args = $this->parseArguments($args, 3);
|
|
break;
|
|
|
|
// Remove the target from responses
|
|
default:
|
|
$args = substr($args, strpos($args, ' ') + 1);
|
|
break;
|
|
}
|
|
|
|
// Create, populate, and return an event object
|
|
if (ctype_digit($cmd)) {
|
|
$event = new Phergie_Event_Response;
|
|
$event
|
|
->setCode($cmd)
|
|
->setDescription($args);
|
|
} else {
|
|
$event = new Phergie_Event_Request;
|
|
$event
|
|
->setType($cmd)
|
|
->setArguments($args);
|
|
if (isset($hostmask)) {
|
|
$event->setHostmask($hostmask);
|
|
}
|
|
}
|
|
$event->setRawData($buffer);
|
|
return $event;
|
|
}
|
|
|
|
/**
|
|
* Initiates a connection with the server.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function doConnect()
|
|
{
|
|
// Listen for input indefinitely
|
|
set_time_limit(0);
|
|
|
|
// Get connection information
|
|
$connection = $this->getConnection();
|
|
$hostname = $connection->getHost();
|
|
$port = $connection->getPort();
|
|
$password = $connection->getPassword();
|
|
$username = $connection->getUsername();
|
|
$nick = $connection->getNick();
|
|
$realname = $connection->getRealname();
|
|
$transport = $connection->getTransport();
|
|
|
|
// Establish and configure the socket connection
|
|
$remote = $transport . '://' . $hostname . ':' . $port;
|
|
$this->socket = @stream_socket_client($remote, $errno, $errstr);
|
|
if (!$this->socket) {
|
|
throw new Phergie_Driver_Exception(
|
|
'Unable to connect: socket error ' . $errno . ' ' . $errstr,
|
|
Phergie_Driver_Exception::ERR_CONNECTION_ATTEMPT_FAILED
|
|
);
|
|
}
|
|
|
|
$seconds = (int) $this->timeout;
|
|
$microseconds = ($this->timeout - $seconds) * 1000000;
|
|
stream_set_timeout($this->socket, $seconds, $microseconds);
|
|
|
|
// Send the password if one is specified
|
|
if (!empty($password)) {
|
|
$this->send('PASS', $password);
|
|
}
|
|
|
|
// Send user information
|
|
$this->send(
|
|
'USER',
|
|
array(
|
|
$username,
|
|
$hostname,
|
|
$hostname,
|
|
$realname
|
|
)
|
|
);
|
|
|
|
$this->send('NICK', $nick);
|
|
|
|
// Add the socket handler to the internal array for socket handlers
|
|
$this->sockets[(string) $connection->getHostmask()] = $this->socket;
|
|
}
|
|
|
|
/**
|
|
* Terminates the connection with the server.
|
|
*
|
|
* @param string $reason Reason for connection termination (optional)
|
|
*
|
|
* @return void
|
|
*/
|
|
public function doQuit($reason = null)
|
|
{
|
|
// Send a QUIT command to the server
|
|
$this->send('QUIT', $reason);
|
|
|
|
// Terminate the socket connection
|
|
fclose($this->socket);
|
|
|
|
// Remove the socket from the internal socket list
|
|
unset($this->sockets[(string) $this->getConnection()->getHostmask()]);
|
|
}
|
|
|
|
/**
|
|
* Joins a channel.
|
|
*
|
|
* @param string $channels Comma-delimited list of channels to join
|
|
* @param string $keys Optional comma-delimited list of channel keys
|
|
*
|
|
* @return void
|
|
*/
|
|
public function doJoin($channels, $keys = null)
|
|
{
|
|
$args = array($channels);
|
|
|
|
if (!empty($keys)) {
|
|
$args[] = $keys;
|
|
}
|
|
|
|
$this->send('JOIN', $args);
|
|
}
|
|
|
|
/**
|
|
* Leaves a channel.
|
|
*
|
|
* @param string $channels Comma-delimited list of channels to leave
|
|
*
|
|
* @return void
|
|
*/
|
|
public function doPart($channels)
|
|
{
|
|
$this->send('PART', $channels);
|
|
}
|
|
|
|
/**
|
|
* Invites a user to an invite-only channel.
|
|
*
|
|
* @param string $nick Nick of the user to invite
|
|
* @param string $channel Name of the channel
|
|
*
|
|
* @return void
|
|
*/
|
|
public function doInvite($nick, $channel)
|
|
{
|
|
$this->send('INVITE', array($nick, $channel));
|
|
}
|
|
|
|
/**
|
|
* Obtains a list of nicks of usrs in currently joined channels.
|
|
*
|
|
* @param string $channels Comma-delimited list of one or more channels
|
|
*
|
|
* @return void
|
|
*/
|
|
public function doNames($channels)
|
|
{
|
|
$this->send('NAMES', $channels);
|
|
}
|
|
|
|
/**
|
|
* Obtains a list of channel names and topics.
|
|
*
|
|
* @param string $channels Comma-delimited list of one or more channels
|
|
* to which the response should be restricted
|
|
* (optional)
|
|
*
|
|
* @return void
|
|
*/
|
|
public function doList($channels = null)
|
|
{
|
|
$this->send('LIST', $channels);
|
|
}
|
|
|
|
/**
|
|
* Retrieves or changes a channel topic.
|
|
*
|
|
* @param string $channel Name of the channel
|
|
* @param string $topic New topic to assign (optional)
|
|
*
|
|
* @return void
|
|
*/
|
|
public function doTopic($channel, $topic = null)
|
|
{
|
|
$args = array($channel);
|
|
|
|
if (!empty($topic)) {
|
|
$args[] = $topic;
|
|
}
|
|
|
|
$this->send('TOPIC', $args);
|
|
}
|
|
|
|
/**
|
|
* Retrieves or changes a channel or user mode.
|
|
*
|
|
* @param string $target Channel name or user nick
|
|
* @param string $mode New mode to assign (optional)
|
|
*
|
|
* @return void
|
|
*/
|
|
public function doMode($target, $mode = null)
|
|
{
|
|
$args = array($target);
|
|
|
|
if (!empty($mode)) {
|
|
$args[] = $mode;
|
|
}
|
|
|
|
$this->send('MODE', $args);
|
|
}
|
|
|
|
/**
|
|
* Changes the client nick.
|
|
*
|
|
* @param string $nick New nick to assign
|
|
*
|
|
* @return void
|
|
*/
|
|
public function doNick($nick)
|
|
{
|
|
$this->send('NICK', $nick);
|
|
}
|
|
|
|
/**
|
|
* Retrieves information about a nick.
|
|
*
|
|
* @param string $nick Nick
|
|
*
|
|
* @return void
|
|
*/
|
|
public function doWhois($nick)
|
|
{
|
|
$this->send('WHOIS', $nick);
|
|
}
|
|
|
|
/**
|
|
* Sends a message to a nick or channel.
|
|
*
|
|
* @param string $target Channel name or user nick
|
|
* @param string $text Text of the message to send
|
|
*
|
|
* @return void
|
|
*/
|
|
public function doPrivmsg($target, $text)
|
|
{
|
|
$this->send('PRIVMSG', array($target, $text));
|
|
}
|
|
|
|
/**
|
|
* Sends a notice to a nick or channel.
|
|
*
|
|
* @param string $target Channel name or user nick
|
|
* @param string $text Text of the notice to send
|
|
*
|
|
* @return void
|
|
*/
|
|
public function doNotice($target, $text)
|
|
{
|
|
$this->send('NOTICE', array($target, $text));
|
|
}
|
|
|
|
/**
|
|
* Kicks a user from a channel.
|
|
*
|
|
* @param string $nick Nick of the user
|
|
* @param string $channel Channel name
|
|
* @param string $reason Reason for the kick (optional)
|
|
*
|
|
* @return void
|
|
*/
|
|
public function doKick($nick, $channel, $reason = null)
|
|
{
|
|
$args = array($nick, $channel);
|
|
|
|
if (!empty($reason)) {
|
|
$args[] = $response;
|
|
}
|
|
|
|
$this->send('KICK', $args);
|
|
}
|
|
|
|
/**
|
|
* Responds to a server test of client responsiveness.
|
|
*
|
|
* @param string $daemon Daemon from which the original request originates
|
|
*
|
|
* @return void
|
|
*/
|
|
public function doPong($daemon)
|
|
{
|
|
$this->send('PONG', $daemon);
|
|
}
|
|
|
|
/**
|
|
* Sends a CTCP ACTION (/me) command to a nick or channel.
|
|
*
|
|
* @param string $target Channel name or user nick
|
|
* @param string $text Text of the action to perform
|
|
*
|
|
* @return void
|
|
*/
|
|
public function doAction($target, $text)
|
|
{
|
|
$buffer = rtrim('ACTION ' . $text);
|
|
|
|
$this->doPrivmsg($target, chr(1) . $buffer . chr(1));
|
|
}
|
|
|
|
/**
|
|
* Sends a CTCP response to a user.
|
|
*
|
|
* @param string $nick User nick
|
|
* @param string $command Command to send
|
|
* @param string|array $args String or array of sequential arguments
|
|
* (optional)
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function doCtcp($nick, $command, $args = null)
|
|
{
|
|
if (is_array($args)) {
|
|
$args = implode(' ', $args);
|
|
}
|
|
|
|
$buffer = rtrim(strtoupper($command) . ' ' . $args);
|
|
|
|
$this->doNotice($nick, chr(1) . $buffer . chr(1));
|
|
}
|
|
|
|
/**
|
|
* Sends a CTCP PING request or response (they are identical) to a user.
|
|
*
|
|
* @param string $nick User nick
|
|
* @param string $hash Hash to use in the handshake
|
|
*
|
|
* @return void
|
|
*/
|
|
public function doPing($nick, $hash)
|
|
{
|
|
$this->doCtcp($nick, 'PING', $hash);
|
|
}
|
|
|
|
/**
|
|
* Sends a CTCP VERSION request or response to a user.
|
|
*
|
|
* @param string $nick User nick
|
|
* @param string $version Version string to send for a response
|
|
*
|
|
* @return void
|
|
*/
|
|
public function doVersion($nick, $version = null)
|
|
{
|
|
if ($version) {
|
|
$this->doCtcp($nick, 'VERSION', $version);
|
|
} else {
|
|
$this->doCtcp($nick, 'VERSION');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sends a CTCP TIME request to a user.
|
|
*
|
|
* @param string $nick User nick
|
|
* @param string $time Time string to send for a response
|
|
*
|
|
* @return void
|
|
*/
|
|
public function doTime($nick, $time = null)
|
|
{
|
|
if ($time) {
|
|
$this->doCtcp($nick, 'TIME', $time);
|
|
} else {
|
|
$this->doCtcp($nick, 'TIME');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sends a CTCP FINGER request to a user.
|
|
*
|
|
* @param string $nick User nick
|
|
* @param string $finger Finger string to send for a response
|
|
*
|
|
* @return void
|
|
*/
|
|
public function doFinger($nick, $finger = null)
|
|
{
|
|
if ($finger) {
|
|
$this->doCtcp($nick, 'FINGER', $finger);
|
|
} else {
|
|
$this->doCtcp($nick, 'FINGER');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sends a raw command to the server.
|
|
*
|
|
* @param string $command Command string to send
|
|
*
|
|
* @return void
|
|
*/
|
|
public function doRaw($command)
|
|
{
|
|
$this->send('RAW', $command);
|
|
}
|
|
}
|