/Missus.php
PHP | 984 lines | 502 code | 64 blank | 418 comment | 87 complexity | 327c25f7bbf2a331e3815d510a8a38ba MD5 | raw file
- <?php
- /*
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *
- * This software consists of voluntary contributions made by many individuals
- * and is licensed under the LGPL. For more information, see
- * <http://www.gnu.org/copyleft/lesser.html>.
- */
- /**
- * @package Missus
- * @author Denis Hovart <dhovart@gmail.com>
- * @license www.gnu.org/copyleft/lesser.html LGPL
- * @version 0.1
- */
- class Missus {
- // =======
- // = Log =
- // =======
- /**
- * @var boolean
- */
- private $log;
- /**
- * @var boolean
- */
- private $log_html;
- // =================
- // = Configuration =
- // =================
- /**
- * @var boolean
- */
- protected $use_tls;
- /**
- * @var boolean
- */
- protected $use_bosh;
- /**
- * @var boolean
- */
- protected $timeout;
- // ====================
- // = Connection infos =
- // ====================
- /**
- * @var string
- */
- protected $host;
- /**
- * @var Integer
- */
- protected $port;
- /**
- * @var boolean
- */
- protected $is_bot;
- /**
- * @var string
- */
- protected $username;
- /**
- * @var string
- */
- protected $domain;
- /**
- * @var string
- */
- protected $password;
- // ==============
- // = XMPP infos =
- // ==============
- /**
- * @var string
- */
- protected $bare_jid;
- /**
- * @var string
- */
- protected $full_jid;
- /**
- * @var Integer
- */
- protected $id;
- /**
- * @var array
- */
- protected $roster;
- // ===================
- // = Events handling =
- // ===================
- /**
- * @var array
- */
- protected $events;
- /**
- * @var boolean
- */
- protected $authenticated;
- /**
- * @var boolean
- */
- protected $connected;
- /**
- * @var boolean
- */
- protected $tls_is_set;
- /**
- * @var boolean
- */
- protected $id_is_set;
- // ==========
- // = Stream =
- // ==========
- /**
- * @var string
- */
- protected $received;
- // =========
- // = Timer =
- // =========
- /**
- * @var integer
- */
- protected $time_start;
- // ======================================
- // = Missus main API and public methods =
- // ======================================
- /**
- * Constructor
- * @param string $username
- * @param string $password
- * @param string $domain
- * @param string $host If null, same as $domain
- * @param boolean $use_tls If set to true, will attempt a secured encypted connection over TLS (if proposed by server)
- * @param boolean $use_bosh (Not yet implemented)
- * @param Integer $port
- * @return Missus
- */
- public function __construct($username, $password, $domain, $host = null, $use_tls = true, $use_bosh = false, $port = 5222) {
- if(null === $username || null === $password || null === $domain)
- throw new Exception("Username, password and host need to be specified");
- if(null === $host) $this->host = $domain;
- else $this->host = $host;
- $this->port = $port;
- $this->username = $username;
- $this->domain = $domain;
- $this->password = $password;
- $this->resource = "Missus";
- $this->full_jid = sprintf("%s@%s/%s", $this->username, $this->host, $this->resource);
- $this->bare_jid = sprintf("%s@%s", $this->username, $this->host);
- $this->received = '';
- $this->timeout = 20;
- $this->use_tls = $use_tls;
- $this->use_bosh = $use_bosh;
- $this->tls_is_set = false;
- $this->authenticated = false;
- $this->connected = false;
- $this->id_is_set = false;
- $this->events = array();
- $this->log = false;
- $this->lang = 'en';
- $this->is_bot = false;
- $this->roster= array();
- if($this->use_bosh) throw new Exception("Bosh is not supported yet");
- $this->time_start = array_sum(explode(' ', microtime()));
- }
- /**
- * Performed when connecting
- */
- protected function onConnect() {}
- /**
- * Performed while Missus is running
- */
- protected function mainLoop() {}
- /**
- * Performed when receiving something new
- */
- protected function onReceivedContent() {}
- /**
- * Performed when a contact is composing a message
- * @param string $from the user composing
- */
- protected function onUserComposing($from) {}
- /**
- * Performed when a contact paused the composition of his message
- * @param string $from the user composing
- */
- protected function onUserPaused($from) {} # todo
- /**
- * Performed when a message is received
- * @param string $body the contents of the message
- * @param string $from the user sending the message
- * @param string $type the type of message
- * @param string $xml the raw xml of the message
- */
- protected function onReceivedMessage($body, $from, $type, $xml) {}
- /**
- * Performed when a contact send a presence stanza
- * @param string $from the user
- * @param string $status the user status
- * @param string $availability the user availability
- */
- protected function onPresence($from, $status, $availability) {}
- /**
- * Performed when a contact send a subscription request
- * @param string $from the user
- */
- protected function onSubscribe($from) {}
- /**
- * Performed when a contact unsubscribe from the user
- * @param string $from the user
- */
- protected function onUnsubscribe($from) {}
- /**
- * Activate events log
- * @return Missus
- */
- public function logEvents() {
- $this->log = true;
- return $this;
- }
- /**
- * Set client lang
- * @param string $lang The lang you wish to set your content to
- * @link http://www.w3.org/TR/xml/#sec-lang-tag
- * @return Missus
- */
- public function setLang($lang) {
- $this->lang = $lang;
- return $this;
- }
- /**
- * Set the connection as persistent
- * @return Missus
- */
- public function setPersistent() {
- $this->is_bot = true;
- return $this;
- }
- /**
- * Open a new stream to the specified host on the specified port, then attempt authentication
- */
- public function connect() {
- $socket = sprintf('tcp://%s:%d/', $this->host, $this->port);
- if($this->log) Logger::log(sprintf('Connecting to %s...', $socket), $this->log_html);
- $stream = @stream_socket_client($socket, $errno, $errstr, $this->timeout, STREAM_CLIENT_CONNECT);
- if(!$stream) throw new Exception(sprintf('Unable to open stream. Error %d: %s', $errno, $errstr));
- stream_set_blocking($stream, 0);
- $this->stream = $stream;
- $this->initiateXMLStream();
- $this->addEventListener('checkFeatures');
- $this->connected = true;
- $this->onConnect();
- if($this->is_bot) $this->addEventListener('checkStreamContent');
- $this->disconnect();
- }
- /**
- * Close XML stream and shut down socket
- */
- public function disconnect() {
- $this->writeToStream('</stream:stream>');
- stream_socket_shutdown($this->stream, STREAM_SHUT_RDWR);
- }
- /**
- * Defines user presence
- * @param string $type
- * @param string $to
- * @param string $from
- * @param string $availability
- * @param string $status
- * @param string $priority
- * @link http://xmpp.org/rfcs/rfc3921.html#rfc.section.5.5
- * @return Missus
- */
- public function sendPresence($type = null, $to = null, $from = null, $availability = null, $status = null, $priority = null) {
- $presence = new SimpleXMLElement('<presence/>');
- if(null !== $type) $presence->addAttribute('type', $type);
- if(null !== $to) $presence->addAttribute('to', $to);
- if(null !== $from) $presence->addAttribute('from', $from);
- if(null !== $availability) $presence->addChild('show', $availability);
- if(null !== $status) $presence->addChild('status', $status);
- if(null !== $priority) $presence->addChild('priority', $priority);
- $this->writeToStream($this->as_xml($presence));
- unset($presence);
- return $this;
- }
- /**
- * Set user availibility
- * @param string $availability
- * @link http://xmpp.org/rfcs/rfc3921.html#stanzas-presence-children-show
- * @uses sendPresence
- * @return Missus
- */
- public function setAvailability($availability) {
- $this->sendPresence(null, null, null, $availability);
- return $this;
- }
- /**
- * Set user status
- * @param string $status
- * @link http://xmpp.org/rfcs/rfc3921.html#stanzas-presence-children-status
- * @uses sendPresence
- * @return Missus
- */
- public function setStatus($status) {
- $this->sendPresence(null, null, null, null, $status);
- return $this;
- }
- /**
- * Set user priority
- * @param integer $priority
- * @link http://xmpp.org/rfcs/rfc3921.html#stanzas-presence-children-priority
- * @uses sendPresence
- * @return Missus
- */
- public function setPriority($priority) {
- $this->sendPresence(null, null, null, null, null, $priority);
- return $this;
- }
- /**
- * Send a subscription request to a user
- * @param string $to the jid of the user you wish to subscribe to
- * @link http://xmpp.org/rfcs/rfc3921.html#rfc.section.6.1
- * @uses sendPresence
- * @return Missus
- */
- public function subscribe($to, $name = null, $group = null) {
- $this->addToRoster($to);
- $this->sendPresence('subscribe', $to);
- $this->askRoster();
- return $this;
- }
- /**
- * Subscribe back to a user
- * @param string $to the jid of the user you wish to subscribe back to
- * @link http://xmpp.org/rfcs/rfc3921.html#rfc.section.8.3
- * @uses subscribe
- * @uses sendPresence
- * @return Missus
- */
- public function subscribeBack($to, $name = null, $group = null) {
- $this->sendPresence('subscribed', $to);
- $this->subscribe($to);
- return $this;
- }
- /**
- * Decline subscription request
- * @param string $to
- * @link http://xmpp.org/rfcs/rfc3921.html#rfc.section.8.2.1
- * @uses sendPresence
- * @return Missus
- */
- public function declineSubscriptionRequest($from) {
- $this->sendPresence('unsubscribed', $from);
- $this->askRoster();
- return $this;
- }
- /**
- * Unsubscribe from a user
- * @param string $to the jid of the user you wish to unsubscribe from
- * @link http://xmpp.org/rfcs/rfc3921.html#rfc.section.6.1
- * @return Missus
- */
- public function unsubscribe($to) {
- $this->sendPresence('unsubscribe', $to);
- # TODO verify if the user is in the contact's roster before
- $this->removeFromRoster($to);
- $this->askRoster();
- return $this;
- }
- /**
- * Returns user roster
- * @link http://xmpp.org/rfcs/rfc3921.html#rfc.section.5.5
- * @return Array
- */
- public function getRoster() {
- return $this->roster;
- }
- /**
- * Send a message to a user
- * @param string $to The jid of the user you wish to send a message to
- * @param string $body The content of your message
- * @param string $subject If any, the subject of the message
- * @param string $thread If any, the thread the message belongs to
- * @param string $type The type of message. If null, set as 'chat'
- * @param string $payload Eventual XML payload
- * @link http://xmpp.org/rfcs/rfc3921.html#rfc.section.2.1
- */
- public function message($to, $body, $subject = null, $thread = null, $type = null, $payload = null) {
- $type = (null === $type) ? 'normal' : $type;
- $this->writeToStream(sprintf(
- '<message from="%s" to="%s" type="%s"><body>%s</body>%s</message>',
- $this->full_jid, $to, $type, $body, $payload
- ));
- return $this;
- }
- /**
- * Send an HTML message to a user
- * @uses message
- * @param string $contents The content of your message
- * @param string $subject If any, the subject of the message
- * @param string $thread If any, the thread the message belongs to
- * @param string $type The type of message. If null, set as 'chat'
- * @link http://xmpp.org/extensions/xep-0071.html
- */
- public function messageHTML($to, $contents, $subject = null, $thread = null, $type = null) {
- $html = new SimpleXMLElement('<html/>', LIBXML_NOXMLDECL, false, 'http://jabber.org/protocol/xhtml-im');
- $html->addChild('body', $contents, 'http://www.w3.org/1999/xhtml');
- $body = filter_var($contents, FILTER_SANITIZE_STRING); # strip HTML tags to include default message also
- $this->message($to, $body, $subject, $thread, $type, $this->as_xml($html));
- }
- /**
- * Get a username (if any) by its jid
- */
- public function getUserByJID($jid) {} # TODO
- // ===================
- // = XML operations =
- // ===================
- /**
- * Loads an xml string and turns it into a SimpleXMLElement object.
- * @return SimpleXMLElement|false
- */
- protected function load_xml_string($string, $wrap = false, $strip_ns = false) {
- if($wrap) $string = '<xml>' . $string . '</xml>';
- if($strip_ns) $string = $this->strip_ns($string);
- # we don't want to validate the (possibly broken) xml parsed (thus the use of ~LIBXML_DTDVALID)
- return @simplexml_load_string($string, 'SimpleXMLElement', ~LIBXML_DTDVALID);
- }
- /**
- * Output a simpleXMLObject as an xml string
- * I had to create my own method since the option LIBXML_NOXMLDECL wasn't ever implemented
- * http://bugs.php.net/bug.php?id=47137 | http://bugs.php.net/bug.php?id=50989
- * @return string
- */
- protected function as_xml($sxe) {
- return preg_replace('/^\<\?xml[^>]*>/', '', $sxe->asXML());
- }
- /**
- * Strip namespaces declarations; not very standard but it can cause some
- * problem for parsing with SimpleXML otherwise.
- */
- protected function strip_ns($string) {
- return preg_replace('/\s(xmlns=(?:"|\')[^"\']*(?:"|\'))/', '', $string);
- }
- // ==================
- // = Events methods =
- // ==================
- /**
- * Get elapsed time
- */
- protected function getElapsedTime() {
- $time = array_sum(explode(' ', microtime()));
- return round($time - $this->time_start, 5);
- }
- /**
- * Adds a "listener", that will call a custom-defined function that checks if an event occured.
- * You can then trigger whatever function you want as a callback in the function you just defined.
- * @param string $eventName The name of the event and of the function (must be of protected access) that you want to call
- */
- protected function addEventListener($eventName) {
- if($this->log) Logger::log('Adding event listener : ' . $eventName, $this->log_html);
- array_unshift($this->events, $eventName);
- $this->readFromStream();
- }
- /**
- * Remove last event
- */
- protected function removeEventListener() {
- if($this->log) Logger::log('Removing event listener : ' . $this->events[0], $this->log_html);
- array_shift($this->events);
- }
- /**
- * Execute the listeners in the events stack
- */
- private function listen() {
- if(preg_match("#^[\s]*$#", $this->received)) return;
- foreach($this->events as $event) call_user_func(array($this, $event));
- }
- // ==============================
- // = Stream read/write methods =
- // ==============================
- /**
- * Write to the stream
- * @param string $contents
- */
- protected function writeToStream($contents) {
- if($this->log) Logger::log('Sending : ' . $contents, $this->log_html);
- stream_socket_sendto($this->stream, $contents);
- }
- /**
- * Read from the stream
- * @param integer $buffer The length of the max amount of bytes you want to read from the stream
- */
- protected function readFromStream($buffer = 8192) {
- $this->emptyReceivedContent();
- if($this->log) Logger::log('Waiting for new content', $this->log_html);
- $write = $except = null;
- while(count($this->events)) {
- $read = array($this->stream);
- $updated_streams = stream_select($read, $write, $except, $this->is_bot ? 0 : $this->timeout);
- if($updated_streams === 0 && !$this->is_bot) throw new Exception('Timeout - server did not answer');
- $this->received .= stream_socket_recvfrom($this->stream, $buffer);
- if($this->connected) $this->mainLoop();
- $this->listen();
- usleep(0020000);
- }
- return true;
- }
- /**
- * Clean "buffer"
- */
- protected function emptyReceivedContent() {
- $this->received = '';
- }
- // ================
- // = XMPP Methods =
- // ================
- /**
- * Sends a new XML stream to server
- * @link http://tools.ietf.org/html/rfc3920#section-4
- */
- protected function initiateXMLStream() {
- $this->writeToStream(sprintf(
- "<?xml version='1.0'?><stream:stream xmlns='jabber:client' ".
- "xmlns:stream='http://etherx.jabber.org/streams' to='%s' xml:lang='%s' version='1.0'>",
- $this->domain, $this->lang
- ));
- }
- /**
- * Handle <stream:features/> tag
- * @link http://tools.ietf.org/html/rfc3920#section-4.6
- */
- private function checkFeatures() {
- $xml = $this->load_xml_string($this->received . '</stream:stream>');
- if(!$xml) return;
- $xml->registerXPathNamespace('stream', 'http://etherx.jabber.org/streams');
- if($features_query = $xml->xpath('/stream:stream/stream:features')) {
- if($this->log) Logger::log('Received : ' . $this->received, $this->log_html);
- $this->removeEventListener();
- $features = $features_query[0];
- if(count($features->bind) && $this->authenticated) $this->bindResource();
- else if(count($features->starttls) && $this->use_tls && !$this->tls_is_set) $this->askTLSConnection();
- else if(count($features->mechanisms)) {
- $mechs = array();
- foreach($features->mechanisms[0] as $mech) $mechs[] = strval($mech);
- $this->handleMechs($mechs);
- }
- else throw new Exception('No authentication mechanisms found');
- }
- unset($xml);
- }
- /**
- * Handle authentication mechanisms defined in stream features
- * Currently supported : PLAIN, DIGEST-MD5, ANONYMOUS
- * @link http://tools.ietf.org/html/rfc3920#section-5.3
- */
- private function handleMechs($mechs) {
- if(empty($mechs)) throw new Exception('No authentication mechanisms found');
- if($this->log) Logger::log('Authentification mechanisms are : ' . implode('; ', $mechs), $this->log_html);
- if(in_array('DIGEST-MD5', $mechs)) $this->digestAuth();
- else if(in_array('PLAIN', $mechs)) $this->plainTextAuth();
- #else if(in_array('ANONYMOUS', $mechs)) $this->anonymousAuth(); # TODO
- }
- /**
- * Ask for TLS connection
- * @link http://tools.ietf.org/html/rfc3920#section-5.1
- */
- private function askTLSConnection() {
- if($this->log) Logger::log('Asking for TLS connection...', $this->log_html);
- $this->writeToStream("<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'><required /></starttls>");
- $this->addEventListener('checkTLSNegotiation');
- }
- /**
- * Custom "listener" to check if server allow us to proceed to stream encryption
- * @link http://tools.ietf.org/html/rfc3920#section-5.1
- */
- private function checkTLSNegotiation() {
- $xml = $this->load_xml_string($this->received);
- switch($xml->getName()) {
- case 'proceed' :
- if($this->log) Logger::log('Received : ' . $this->received, $this->log_html);
- $this->removeEventListener();
- if($this->log) Logger::log('Proceeding to TLS encrypted connection', $this->log_html);
- $this->startTLSConnection();
- break;
- default:
- case 'failure' :
- if($this->log) Logger::log('Received : ' . $this->received, $this->log_html);
- $this->removeEventListener();
- throw new Exception('Unable to connect over TLS');
- break;
- }
- unset($xml);
- }
- /**
- * Starts stream encryption
- * @link http://tools.ietf.org/html/rfc3920#section-5.1
- */
- private function startTLSConnection() {
- stream_socket_enable_crypto($this->stream, true, STREAM_CRYPTO_METHOD_SSLv23_CLIENT);
- $this->tls_is_set = true;
- $this->initiateXMLStream();
- $this->addEventListener('checkFeatures');
- }
- /**
- * Ask for digest-md5 authentication
- */
- private function digestAuth() {
- if($this->log) Logger::log('Attempting DIGEST-MD5 authentication... ', $this->log_html);
- $this->writeToStream('<auth xmlns="urn:ietf:params:xml:ns:xmpp-sasl" mechanism="DIGEST-MD5"/>');
- $this->addEventListener('checkChallenge');
- }
- /**
- * Check if we received a challenge from the server (second part of the digest-md5 authentication routine)
- */
- private function checkChallenge() {
- if($this->log) Logger::log('Received : ' . $this->received, $this->log_html);
- $xml = $this->load_xml_string($this->received);
- $this->removeEventListener();
- $this->processChallenge(base64_decode(strval($xml[0])));
- }
- /**
- * Process the challenge received and send a response to the server (third part of the digest-md5 authentication routine)
- * @param $challenge
- */
- private function processChallenge($challenge) {
- if($this->log) Logger::log('Processing received challenge...', $this->log_html);
- $challenge_contents = explode(',', $challenge);
- $challenge_assoc = array();
- foreach($challenge_contents as $content) {
- preg_match('#(?P<key>.+)=(?P<value>.+)#', $content, $matches);
- $challenge_assoc[$matches['key']] = str_replace('"', '', $matches['value']);
- }
- $cnonce = md5(rand().time());
- $digest_uri = sprintf('xmpp/%s', $this->host);
- $realm = isset($challenge_assoc['realm']) ? $challenge_assoc['realm'] : '';
- $response = $this->generateDigestResponse(
- $this->username,
- $this->password,
- $realm,
- $challenge_assoc['nonce'],
- $cnonce,
- $digest_uri,
- $this->domain == 'chat.facebook.com' ? false : $this->full_jid
- );
- if(!isset($challenge_assoc['realm'])) $response_content = sprintf(
- 'username="%s",nonce="%s",cnonce="%s",nc=00000001,qop=auth,digest-uri="%s",response=%s,charset=utf-8',
- $this->username, $challenge_assoc['nonce'], $cnonce, $digest_uri, $response, $this->full_jid
- );
- else $response_content = sprintf(
- 'username="%s",realm="%s",nonce="%s",cnonce="%s",nc=00000001,qop=auth,digest-uri="%s",response=%s,charset=utf-8',
- $this->username, $challenge_assoc['realm'], $challenge_assoc['nonce'], $cnonce, $digest_uri, $response, $this->full_jid
- );
- if($this->domain != 'chat.facebook.com') $response_content .= sprintf(',authzid="%s"', $this->full_jid);
- $this->writeToStream(sprintf(
- '<response xmlns="urn:ietf:params:xml:ns:xmpp-sasl">%s</response>',
- base64_encode($response_content)
- ));
- $this->addEventListener('checkChallenge2');
- }
- /**
- * Helper method that generates the 'response' part of our answer to the server
- */
- private function generateDigestResponse($username, $pass, $realm, $nonce, $cnonce, $digest_uri, $authzid = false) {
- if($authzid) $A1 = sprintf('%s:%s:%s:%s', pack('H32', md5(sprintf('%s:%s:%s', $username, $realm, $pass))), $nonce, $cnonce, $authzid);
- else $A1 = sprintf('%s:%s:%s', pack('H32', md5(sprintf('%s:%s:%s', $username, $realm, $pass))), $nonce, $cnonce);
- $A2 = 'AUTHENTICATE:' . $digest_uri;
- return md5(sprintf('%s:%s:00000001:%s:auth:%s', md5($A1), $nonce, $cnonce, md5($A2)));
- }
- /**
- * Check if we received a second challenge from the server then wait for authentication confirmation
- */
- private function checkChallenge2() {
- if($this->log) Logger::log('Received : ' . $this->received, $this->log_html);
- $xml = $this->load_xml_string($this->received);
- $this->removeEventListener();
- if($xml[0]->getName() === 'failure') throw new Exception('Authentication failed');
- $this->writeToStream('<response xmlns="urn:ietf:params:xml:ns:xmpp-sasl"></response>');
- $this->addEventListener('checkAuthConfirmation');
- }
- /**
- * Send authentication credentials as plain-text then wait for authentication confirmation (fourth part of the digest-md5 authentication routine)
- */
- private function plainTextAuth() {
- if($this->log) Logger::log('Attempting plain-text authentication... ', $this->log_html);
- $this->writeToStream(sprintf(
- "<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>%s</auth>",
- base64_encode(chr(0) . $this->username . chr(0) . $this->password)
- ));
- $this->addEventListener('checkAuthConfirmation');
- }
- /**
- * Check if the server successfully authenticated the user, whatever the mechanism used was
- */
- private function checkAuthConfirmation() {
- if($this->log) Logger::log('Received : ' . $this->received, $this->log_html);
- $xml = $this->load_xml_string($this->received);
- switch($xml[0]->getName()) {
- default:
- case 'failure':
- $children = $xml[0]->children();
- throw new Exception('Unable to authenticate : ' . $children[0]->getName());
- break;
- case 'success':
- if($this->log) Logger::log('Successfully authenticated ', $this->log_html);
- $this->authenticated = true;
- $this->removeEventListener();
- $this->initiateXMLStream();
- $this->addEventListener('checkFeatures');
- break;
- }
- unset($xml);
- }
- /**
- * Helper method that generates a unique id for the user and register it in the class attributes
- */
- private function generateId() {
- $this->id = uniqid();
- $this->id_is_set = true;
- }
- /**
- * Ask for binding resource id on the server
- * @link http://tools.ietf.org/html/rfc3920#section-7
- */
- private function bindResource() {
- $this->generateId();
- if($this->log) Logger::log('Attempting to bind resource on server...', $this->log_html);
- $this->writeToStream(sprintf(
- "<iq type='set' id='%s'>".
- "<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'>".
- "<resource>%s</resource>" .
- "</bind>" .
- "</iq>",
- $this->id,
- $this->resource
- ));
- $this->addEventListener('checkBoundResourceResult');
- }
- /**
- * Check if resource identifier was successfully bound on the server
- * @link http://tools.ietf.org/html/rfc3920#section-7
- */
- private function checkBoundResourceResult() {
- if($this->log) Logger::log('Received : ' . $this->received, $this->log_html);
- $xml = $this->load_xml_string($this->received);
- $iq_type_query = $xml->xpath('@type');
- switch($iq_type_query[0]) {
- case 'result':
- if($this->log) Logger::log('Resource identifier successfully bound on server', $this->log_html);
- $this->removeEventListener();
- $this->full_jid = strval($xml[0]->bind->jid);
- $this->startSession();
- break;
- default:
- case 'error':
- $error_query = $xml[0]->error->children();
- $error = $error_query[0];
- switch($error->getName()) {
- case 'bad-request':
- throw new Exception('Resource identifier cannot be processed');
- break;
- case 'conflict':
- throw new Exception('Resource identifier id already in use');
- break;
- case 'not-allowed':
- throw new Exception('Not allowed to bind a resource');
- break;
- }
- break;
- }
- unset($xml);
- }
- /**
- * Ask for a new session, then wait confirmation from server
- * @link http://xmpp.org/rfcs/rfc3921.html#session
- */
- private function startSession() {
- $this->generateId();
- $this->writeToStream(sprintf(
- "<iq xmlns='jabber:client' type='set' id='%s'>" .
- "<session xmlns='urn:ietf:params:xml:ns:xmpp-session' />" .
- "</iq>",
- $this->id
- ));
- $this->addEventListener('checkSessionStarted');
- }
- /**
- * Check if session started
- * @link http://xmpp.org/rfcs/rfc3921.html#session
- */
- private function checkSessionStarted() {
- $xml = $this->load_xml_string($this->received);
- if(!$xml) return;
- if($this->log) Logger::log('Received : ' . $this->received, $this->log_html);
- $result = strval($xml->attributes()->type);
- if($result == 'result') {
- if($this->log) Logger::log('Session started', $this->log_html);
- $this->removeEventListener();
- $this->askRoster();
- }
- else {
- # TODO : get error type
- throw new Exception('Unable to initiate session');
- }
- }
- /**
- * Ask for user roster
- * @link http://xmpp.org/rfcs/rfc3921.html#rfc.section.7.3
- */
- private function askRoster() {
- $this->writeToStream(sprintf(
- "<iq from='%s' type='get' id='roster_1'><query xmlns='jabber:iq:roster'/></iq>",
- $this->full_jid
- ));
- $this->addEventListener('getRosterContents');
- }
- /**
- * Store user roster in an associative array
- * @link http://xmpp.org/rfcs/rfc3921.html#rfc.section.7.3
- */
- private function getRosterContents() {
- $xml = $this->load_xml_string($this->received, true, true);
- if(!$xml) return;
- if($this->log) Logger::log('Received : ' . $this->received, $this->log_html);
- $result = $xml->xpath("//iq[@type='result']/query");
- if(!$result) return;
- else $contacts = $result[0]->children();
- foreach($contacts as $contact) {
- $attrs = $contact->attributes();
- $this->roster[strval($attrs->jid)] = array(
- 'subscription' => strval($attrs->subscription),
- 'name' => isset($attrs->name) ? strval($attrs->name) : null
- );
- }
- $this->removeEventListener();
- }
- /**
- *
- * @link
- */
- private function addToRoster($jid, $name = null, $group = null) {
- $name = ($name !== null) ? $name : $jid;
- $group_str = '';
- if($group !== null) {
- if(is_array($group)) foreach($group as $g) $group_str .= '<group>' . $g . '</group>';
- else $group_str = $group;
- }
- $this->writeToStream(sprintf(
- "<iq from='%s' type='set' id='set1'><query xmlns='jabber:iq:roster'>" .
- "<item jid='%s' name='%s'>%s</item></query></iq>",
- $this->full_jid, $jid, $name, $group_str
- ));
- }
- /**
- *
- * @link
- */
- private function removeFromRoster($jid) {
- $this->writeToStream(sprintf(
- "<iq from='%s' id='roster_4' type='set'><query xmlns='jabber:iq:roster'><item subscription='remove' jid='%s'/></query></iq>",
- $this->full_jid, $jid
- ));
- }
- // =================
- // = Bot behaviour =
- // =================
- /**
- * This function will be called in the main loop for persistent connections
- */
- private function checkStreamContent() {
- # Logger::log($this->received, false);
- $xml = $this->load_xml_string($this->received, true, true);
- $messages = $xml->xpath("//message");
- foreach($messages as $msg) {
- $attrs = $msg->attributes();
- $children = $msg->children();
- $body = $children->body;
- if(!empty($body)) {
- $this->onReceivedMessage(strval($body), strval($attrs->from), strval($attrs->type), $msg->asXML());
- continue;
- }
- if(isset($children->composing)) $this->onUserComposing(strval($attrs->from));
- else if(isset($children->paused)) $this->onUserPaused(strval($attrs->from));
- }
- $presences = $xml->xpath("//presence");
- foreach($presences as $presence) {
- $attrs = $presence->attributes();
- $children = $presence->children();
- $status = isset($children->status) ? strval($children->status) : null;
- $availability = isset($children->show) ? strval($children->show) : null;
- if($attrs->type == 'subscribe') $this->onSubscribe(strval($attrs->from));
- else if($attrs->type == 'unsubscribe') $this->onUnsubscribe(strval($attrs->from));
- else $this->onPresence(strval($attrs->from), $status, $availability);
- }
- $this->onReceivedContent();
- $this->emptyReceivedContent();
- }
- }
- /**
- * A very simple logger.
- * @package Logger
- */
- class Logger {
- static public function log($log, $html = false) {
- echo '__________'. ($html ? "\n<br />" : "\n");
- echo '['. strftime('%H:%M:%S') . '] ' . ($html ? htmlentities($log) : $log) . ($html ? "\n<br />" : "\n");
- }
- }
- ?>