forked from GNUsocial/gnu-social
* extlib/Stomp.php -spaces for tabs (we're on PEAR, right?) - send: initialize the $properties parameter as array() instead of null this prevents unsetting $headers if $properties was not set (besides that, it's the proper way to initialize an array) - subscribe: insert FIXME's on ActiveMQ specifics - ack: make sure the content-length header is set *and* is zero. I have seen the header set to '3' there but could not find where it came from, this is at least safe. - disconnect: typo in $headers variable - readFrame: use fgets() instead of gets() so that RabbitQ, which is more protocol strict can also play * extlib/Stomp/Frame.php - spaces for tabs - add note on possibly protocol violating linefeed * extlib/Stomp/Message.php - space for tabs - add content-length header for message * lib/stompqueuemanager.php - use the notice for logging, not the frame
613 lines
18 KiB
PHP
613 lines
18 KiB
PHP
<?php
|
|
/**
|
|
*
|
|
* Copyright 2005-2006 The Apache Software Foundation
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
/* vim: set expandtab tabstop=3 shiftwidth=3: */
|
|
|
|
require_once 'Stomp/Frame.php';
|
|
|
|
/**
|
|
* A Stomp Connection
|
|
*
|
|
*
|
|
* @package Stomp
|
|
* @author Hiram Chirino <hiram@hiramchirino.com>
|
|
* @author Dejan Bosanac <dejan@nighttale.net>
|
|
* @author Michael Caplan <mcaplan@labnet.net>
|
|
* @version $Revision: 43 $
|
|
*/
|
|
class Stomp
|
|
{
|
|
/**
|
|
* Perform request synchronously
|
|
*
|
|
* @var boolean
|
|
*/
|
|
public $sync = false;
|
|
|
|
/**
|
|
* Default prefetch size
|
|
*
|
|
* @var int
|
|
*/
|
|
public $prefetchSize = 1;
|
|
|
|
/**
|
|
* Client id used for durable subscriptions
|
|
*
|
|
* @var string
|
|
*/
|
|
public $clientId = null;
|
|
|
|
protected $_brokerUri = null;
|
|
protected $_socket = null;
|
|
protected $_hosts = array();
|
|
protected $_params = array();
|
|
protected $_subscriptions = array();
|
|
protected $_defaultPort = 61613;
|
|
protected $_currentHost = - 1;
|
|
protected $_attempts = 10;
|
|
protected $_username = '';
|
|
protected $_password = '';
|
|
protected $_sessionId;
|
|
protected $_read_timeout_seconds = 60;
|
|
protected $_read_timeout_milliseconds = 0;
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @param string $brokerUri Broker URL
|
|
* @throws Stomp_Exception
|
|
*/
|
|
public function __construct ($brokerUri)
|
|
{
|
|
$this->_brokerUri = $brokerUri;
|
|
$this->_init();
|
|
}
|
|
/**
|
|
* Initialize connection
|
|
*
|
|
* @throws Stomp_Exception
|
|
*/
|
|
protected function _init ()
|
|
{
|
|
$pattern = "|^(([a-zA-Z]+)://)+\(*([a-zA-Z0-9\.:/i,-]+)\)*\??([a-zA-Z0-9=]*)$|i";
|
|
if (preg_match($pattern, $this->_brokerUri, $regs)) {
|
|
$scheme = $regs[2];
|
|
$hosts = $regs[3];
|
|
$params = $regs[4];
|
|
if ($scheme != "failover") {
|
|
$this->_processUrl($this->_brokerUri);
|
|
} else {
|
|
$urls = explode(",", $hosts);
|
|
foreach ($urls as $url) {
|
|
$this->_processUrl($url);
|
|
}
|
|
}
|
|
if ($params != null) {
|
|
parse_str($params, $this->_params);
|
|
}
|
|
} else {
|
|
require_once 'Stomp/Exception.php';
|
|
throw new Stomp_Exception("Bad Broker URL {$this->_brokerUri}");
|
|
}
|
|
}
|
|
/**
|
|
* Process broker URL
|
|
*
|
|
* @param string $url Broker URL
|
|
* @throws Stomp_Exception
|
|
* @return boolean
|
|
*/
|
|
protected function _processUrl ($url)
|
|
{
|
|
$parsed = parse_url($url);
|
|
if ($parsed) {
|
|
array_push($this->_hosts, array($parsed['host'] , $parsed['port'] , $parsed['scheme']));
|
|
} else {
|
|
require_once 'Stomp/Exception.php';
|
|
throw new Stomp_Exception("Bad Broker URL $url");
|
|
}
|
|
}
|
|
/**
|
|
* Make socket connection to the server
|
|
*
|
|
* @throws Stomp_Exception
|
|
*/
|
|
protected function _makeConnection ()
|
|
{
|
|
if (count($this->_hosts) == 0) {
|
|
require_once 'Stomp/Exception.php';
|
|
throw new Stomp_Exception("No broker defined");
|
|
}
|
|
|
|
// force disconnect, if previous established connection exists
|
|
$this->disconnect();
|
|
|
|
$i = $this->_currentHost;
|
|
$att = 0;
|
|
$connected = false;
|
|
while (! $connected && $att ++ < $this->_attempts) {
|
|
if (isset($this->_params['randomize']) && $this->_params['randomize'] == 'true') {
|
|
$i = rand(0, count($this->_hosts) - 1);
|
|
} else {
|
|
$i = ($i + 1) % count($this->_hosts);
|
|
}
|
|
$broker = $this->_hosts[$i];
|
|
$host = $broker[0];
|
|
$port = $broker[1];
|
|
$scheme = $broker[2];
|
|
if ($port == null) {
|
|
$port = $this->_defaultPort;
|
|
}
|
|
if ($this->_socket != null) {
|
|
fclose($this->_socket);
|
|
$this->_socket = null;
|
|
}
|
|
$this->_socket = @fsockopen($scheme . '://' . $host, $port);
|
|
if (!is_resource($this->_socket) && $att >= $this->_attempts && !array_key_exists($i + 1, $this->_hosts)) {
|
|
require_once 'Stomp/Exception.php';
|
|
throw new Stomp_Exception("Could not connect to $host:$port ($att/{$this->_attempts})");
|
|
} else if (is_resource($this->_socket)) {
|
|
$connected = true;
|
|
$this->_currentHost = $i;
|
|
break;
|
|
}
|
|
}
|
|
if (! $connected) {
|
|
require_once 'Stomp/Exception.php';
|
|
throw new Stomp_Exception("Could not connect to a broker");
|
|
}
|
|
}
|
|
/**
|
|
* Connect to server
|
|
*
|
|
* @param string $username
|
|
* @param string $password
|
|
* @return boolean
|
|
* @throws Stomp_Exception
|
|
*/
|
|
public function connect ($username = '', $password = '')
|
|
{
|
|
$this->_makeConnection();
|
|
if ($username != '') {
|
|
$this->_username = $username;
|
|
}
|
|
if ($password != '') {
|
|
$this->_password = $password;
|
|
}
|
|
$headers = array('login' => $this->_username , 'passcode' => $this->_password);
|
|
if ($this->clientId != null) {
|
|
$headers["client-id"] = $this->clientId;
|
|
}
|
|
$frame = new Stomp_Frame("CONNECT", $headers);
|
|
$this->_writeFrame($frame);
|
|
$frame = $this->readFrame();
|
|
if ($frame instanceof Stomp_Frame && $frame->command == 'CONNECTED') {
|
|
$this->_sessionId = $frame->headers["session"];
|
|
return true;
|
|
} else {
|
|
require_once 'Stomp/Exception.php';
|
|
if ($frame instanceof Stomp_Frame) {
|
|
throw new Stomp_Exception("Unexpected command: {$frame->command}", 0, $frame->body);
|
|
} else {
|
|
throw new Stomp_Exception("Connection not acknowledged");
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if client session has ben established
|
|
*
|
|
* @return boolean
|
|
*/
|
|
public function isConnected ()
|
|
{
|
|
return !empty($this->_sessionId) && is_resource($this->_socket);
|
|
}
|
|
/**
|
|
* Current stomp session ID
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getSessionId()
|
|
{
|
|
return $this->_sessionId;
|
|
}
|
|
/**
|
|
* Send a message to a destination in the messaging system
|
|
*
|
|
* @param string $destination Destination queue
|
|
* @param string|Stomp_Frame $msg Message
|
|
* @param array $properties
|
|
* @param boolean $sync Perform request synchronously
|
|
* @return boolean
|
|
*/
|
|
public function send ($destination, $msg, $properties = array(), $sync = null)
|
|
{
|
|
if ($msg instanceof Stomp_Frame) {
|
|
$msg->headers['destination'] = $destination;
|
|
$msg->headers = array_merge($msg->headers, $properties);
|
|
$frame = $msg;
|
|
} else {
|
|
$headers = $properties;
|
|
$headers['destination'] = $destination;
|
|
$frame = new Stomp_Frame('SEND', $headers, $msg);
|
|
}
|
|
$this->_prepareReceipt($frame, $sync);
|
|
$this->_writeFrame($frame);
|
|
return $this->_waitForReceipt($frame, $sync);
|
|
}
|
|
/**
|
|
* Prepair frame receipt
|
|
*
|
|
* @param Stomp_Frame $frame
|
|
* @param boolean $sync
|
|
*/
|
|
protected function _prepareReceipt (Stomp_Frame $frame, $sync)
|
|
{
|
|
$receive = $this->sync;
|
|
if ($sync !== null) {
|
|
$receive = $sync;
|
|
}
|
|
if ($receive == true) {
|
|
$frame->headers['receipt'] = md5(microtime());
|
|
}
|
|
}
|
|
/**
|
|
* Wait for receipt
|
|
*
|
|
* @param Stomp_Frame $frame
|
|
* @param boolean $sync
|
|
* @return boolean
|
|
* @throws Stomp_Exception
|
|
*/
|
|
protected function _waitForReceipt (Stomp_Frame $frame, $sync)
|
|
{
|
|
|
|
$receive = $this->sync;
|
|
if ($sync !== null) {
|
|
$receive = $sync;
|
|
}
|
|
if ($receive == true) {
|
|
$id = (isset($frame->headers['receipt'])) ? $frame->headers['receipt'] : null;
|
|
if ($id == null) {
|
|
return true;
|
|
}
|
|
$frame = $this->readFrame();
|
|
if ($frame instanceof Stomp_Frame && $frame->command == 'RECEIPT') {
|
|
if ($frame->headers['receipt-id'] == $id) {
|
|
return true;
|
|
} else {
|
|
require_once 'Stomp/Exception.php';
|
|
throw new Stomp_Exception("Unexpected receipt id {$frame->headers['receipt-id']}", 0, $frame->body);
|
|
}
|
|
} else {
|
|
require_once 'Stomp/Exception.php';
|
|
if ($frame instanceof Stomp_Frame) {
|
|
throw new Stomp_Exception("Unexpected command {$frame->command}", 0, $frame->body);
|
|
} else {
|
|
throw new Stomp_Exception("Receipt not received");
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
/**
|
|
* Register to listen to a given destination
|
|
*
|
|
* @param string $destination Destination queue
|
|
* @param array $properties
|
|
* @param boolean $sync Perform request synchronously
|
|
* @return boolean
|
|
* @throws Stomp_Exception
|
|
*/
|
|
public function subscribe ($destination, $properties = null, $sync = null)
|
|
{
|
|
$headers = array('ack' => 'client');
|
|
// FIXME: this seems to be activemq specific, but not hurting rabbitmq?
|
|
$headers['activemq.prefetchSize'] = $this->prefetchSize;
|
|
if ($this->clientId != null) {
|
|
// FIXME: this seems to be activemq specific, but not hurting rabbitmq?
|
|
$headers["activemq.subcriptionName"] = $this->clientId;
|
|
}
|
|
if (isset($properties)) {
|
|
foreach ($properties as $name => $value) {
|
|
$headers[$name] = $value;
|
|
}
|
|
}
|
|
$headers['destination'] = $destination;
|
|
$frame = new Stomp_Frame('SUBSCRIBE', $headers);
|
|
$this->_prepareReceipt($frame, $sync);
|
|
$this->_writeFrame($frame);
|
|
if ($this->_waitForReceipt($frame, $sync) == true) {
|
|
$this->_subscriptions[$destination] = $properties;
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
/**
|
|
* Remove an existing subscription
|
|
*
|
|
* @param string $destination
|
|
* @param array $properties
|
|
* @param boolean $sync Perform request synchronously
|
|
* @return boolean
|
|
* @throws Stomp_Exception
|
|
*/
|
|
public function unsubscribe ($destination, $properties = null, $sync = null)
|
|
{
|
|
$headers = array();
|
|
if (isset($properties)) {
|
|
foreach ($properties as $name => $value) {
|
|
$headers[$name] = $value;
|
|
}
|
|
}
|
|
$headers['destination'] = $destination;
|
|
$frame = new Stomp_Frame('UNSUBSCRIBE', $headers);
|
|
$this->_prepareReceipt($frame, $sync);
|
|
$this->_writeFrame($frame);
|
|
if ($this->_waitForReceipt($frame, $sync) == true) {
|
|
unset($this->_subscriptions[$destination]);
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
/**
|
|
* Start a transaction
|
|
*
|
|
* @param string $transactionId
|
|
* @param boolean $sync Perform request synchronously
|
|
* @return boolean
|
|
* @throws Stomp_Exception
|
|
*/
|
|
public function begin ($transactionId = null, $sync = null)
|
|
{
|
|
$headers = array();
|
|
if (isset($transactionId)) {
|
|
$headers['transaction'] = $transactionId;
|
|
}
|
|
$frame = new Stomp_Frame('BEGIN', $headers);
|
|
$this->_prepareReceipt($frame, $sync);
|
|
$this->_writeFrame($frame);
|
|
return $this->_waitForReceipt($frame, $sync);
|
|
}
|
|
/**
|
|
* Commit a transaction in progress
|
|
*
|
|
* @param string $transactionId
|
|
* @param boolean $sync Perform request synchronously
|
|
* @return boolean
|
|
* @throws Stomp_Exception
|
|
*/
|
|
public function commit ($transactionId = null, $sync = null)
|
|
{
|
|
$headers = array();
|
|
if (isset($transactionId)) {
|
|
$headers['transaction'] = $transactionId;
|
|
}
|
|
$frame = new Stomp_Frame('COMMIT', $headers);
|
|
$this->_prepareReceipt($frame, $sync);
|
|
$this->_writeFrame($frame);
|
|
return $this->_waitForReceipt($frame, $sync);
|
|
}
|
|
/**
|
|
* Roll back a transaction in progress
|
|
*
|
|
* @param string $transactionId
|
|
* @param boolean $sync Perform request synchronously
|
|
*/
|
|
public function abort ($transactionId = null, $sync = null)
|
|
{
|
|
$headers = array();
|
|
if (isset($transactionId)) {
|
|
$headers['transaction'] = $transactionId;
|
|
}
|
|
$frame = new Stomp_Frame('ABORT', $headers);
|
|
$this->_prepareReceipt($frame, $sync);
|
|
$this->_writeFrame($frame);
|
|
return $this->_waitForReceipt($frame, $sync);
|
|
}
|
|
/**
|
|
* Acknowledge consumption of a message from a subscription
|
|
* Note: This operation is always asynchronous
|
|
*
|
|
* @param string|Stomp_Frame $messageMessage ID
|
|
* @param string $transactionId
|
|
* @return boolean
|
|
* @throws Stomp_Exception
|
|
*/
|
|
public function ack ($message, $transactionId = null)
|
|
{
|
|
// Handle the headers,
|
|
$headers = array();
|
|
|
|
if ($message instanceof Stomp_Frame) {
|
|
// Copy headers from the object
|
|
// FIXME: at least content-length can be wrong here (set to 3 sometimes).
|
|
$headers = $message->headers;
|
|
} else {
|
|
if (isset($transactionId)) {
|
|
$headers['transaction'] = $transactionId;
|
|
}
|
|
$headers['message-id'] = $message;
|
|
}
|
|
// An ACK has no content
|
|
$headers['content-length'] = 0;
|
|
|
|
// Create it and write it out
|
|
$frame = new Stomp_Frame('ACK', $headers);
|
|
$this->_writeFrame($frame);
|
|
return true;
|
|
}
|
|
/**
|
|
* Graceful disconnect from the server
|
|
*
|
|
*/
|
|
public function disconnect ()
|
|
{
|
|
$headers = array();
|
|
|
|
if ($this->clientId != null) {
|
|
$headers["client-id"] = $this->clientId;
|
|
}
|
|
|
|
if (is_resource($this->_socket)) {
|
|
$this->_writeFrame(new Stomp_Frame('DISCONNECT', $headers));
|
|
fclose($this->_socket);
|
|
}
|
|
$this->_socket = null;
|
|
$this->_sessionId = null;
|
|
$this->_currentHost = -1;
|
|
$this->_subscriptions = array();
|
|
$this->_username = '';
|
|
$this->_password = '';
|
|
}
|
|
/**
|
|
* Write frame to server
|
|
*
|
|
* @param Stomp_Frame $stompFrame
|
|
*/
|
|
protected function _writeFrame (Stomp_Frame $stompFrame)
|
|
{
|
|
if (!is_resource($this->_socket)) {
|
|
require_once 'Stomp/Exception.php';
|
|
throw new Stomp_Exception('Socket connection hasn\'t been established');
|
|
}
|
|
|
|
$data = $stompFrame->__toString();
|
|
$r = fwrite($this->_socket, $data, strlen($data));
|
|
if ($r === false || $r == 0) {
|
|
$this->_reconnect();
|
|
$this->_writeFrame($stompFrame);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set timeout to wait for content to read
|
|
*
|
|
* @param int $seconds_to_wait Seconds to wait for a frame
|
|
* @param int $milliseconds Milliseconds to wait for a frame
|
|
*/
|
|
public function setReadTimeout($seconds, $milliseconds = 0)
|
|
{
|
|
$this->_read_timeout_seconds = $seconds;
|
|
$this->_read_timeout_milliseconds = $milliseconds;
|
|
}
|
|
|
|
/**
|
|
* Read responce frame from server
|
|
*
|
|
* @return Stomp_Frame|Stomp_Message_Map|boolean False when no frame to read
|
|
*/
|
|
public function readFrame ()
|
|
{
|
|
if (!$this->hasFrameToRead()) {
|
|
return false;
|
|
}
|
|
|
|
$rb = 1024;
|
|
$data = '';
|
|
do {
|
|
$read = fread($this->_socket, $rb);
|
|
if ($read === false) {
|
|
$this->_reconnect();
|
|
return $this->readFrame();
|
|
}
|
|
$data .= $read;
|
|
$len = strlen($data);
|
|
|
|
$continue = true;
|
|
// ActiveMq apparently add \n after 0 char
|
|
if($data[$len - 2] == "\x00" && $data[$len - 1] == "\n") {
|
|
$continue = false;
|
|
}
|
|
|
|
// RabbitMq does not
|
|
if($data[$len - 1] == "\x00") {
|
|
$continue = false;
|
|
}
|
|
} while ( $continue );
|
|
list ($header, $body) = explode("\n\n", $data, 2);
|
|
$header = explode("\n", $header);
|
|
$headers = array();
|
|
$command = null;
|
|
foreach ($header as $v) {
|
|
if (isset($command)) {
|
|
list ($name, $value) = explode(':', $v, 2);
|
|
$headers[$name] = $value;
|
|
} else {
|
|
$command = $v;
|
|
}
|
|
}
|
|
$frame = new Stomp_Frame($command, $headers, trim($body));
|
|
if (isset($frame->headers['amq-msg-type']) && $frame->headers['amq-msg-type'] == 'MapMessage') {
|
|
require_once 'Stomp/Message/Map.php';
|
|
return new Stomp_Message_Map($frame);
|
|
} else {
|
|
return $frame;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if there is a frame to read
|
|
*
|
|
* @return boolean
|
|
*/
|
|
public function hasFrameToRead()
|
|
{
|
|
$read = array($this->_socket);
|
|
$write = null;
|
|
$except = null;
|
|
|
|
$has_frame_to_read = stream_select($read, $write, $except, $this->_read_timeout_seconds, $this->_read_timeout_milliseconds);
|
|
|
|
if ($has_frame_to_read === false) {
|
|
throw new Stomp_Exception('Check failed to determin if the socket is readable');
|
|
} else if ($has_frame_to_read > 0) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reconnects and renews subscriptions (if there were any)
|
|
* Call this method when you detect connection problems
|
|
*/
|
|
protected function _reconnect ()
|
|
{
|
|
$subscriptions = $this->_subscriptions;
|
|
|
|
$this->connect($this->_username, $this->_password);
|
|
foreach ($subscriptions as $dest => $properties) {
|
|
$this->subscribe($dest, $properties);
|
|
}
|
|
}
|
|
/**
|
|
* Graceful object desruction
|
|
*
|
|
*/
|
|
public function __destruct()
|
|
{
|
|
$this->disconnect();
|
|
}
|
|
}
|
|
?>
|