Categories


Loading feed
Loading feed

AJAX Chat Tutorial Part 6 : Updating the User List


Updating the user list should occur whenever the user adds a new chat message or refreshes the chat window. Since we already use the MessageAction() method on our PHP IndexController class to forward such responses to the browser, we'll simply amend it to also return a list of currently online users. To assess which users are online we may assume each screen name is a separate user. This isn't accurate since a user may change screen names as many times as they wish but will suffice for now. If we maintain the current application design we could improve this by using the PHP Session id as the basis to narrow screen names down to unique users. If we used a database and enforced user accounts, this would not be an issue.

We'll also assume an online user is one who has submitted a message in the previous five minutes. Since both screen names and message timestamps are stored in our XML file, we can use XPATH to capture the necessary data.

Decomposing MessageAction()

We'll start by amending our IndexController code to build an array of online users who have posted messages in the previous five minutes. Our current MessageAction() code is getting a bit cluttered however. To make it simpler to follow we'll refactor the selection of new messages and online users into two new private methods called getNewMessages() and getOnlineUsers().

The revised MessageAction() method now looks like:

public function MessageAction()
{
    /*
     * Check for Session value chat_lrefresh (last refresh timestamp)
     * otherwise set it to time() - 120 (2 minutes earlier)
     */
    if(!isset($_SESSION['chat_lastrefresh']))
    {
        $_SESSION['chat_lastrefresh'] = time() - 1200;
    }
 
    /*
     * At this point the user has submitted a new chat message
     * without setting a screen name. Since the default is in
     * place, we'll simply add to the session for future use.
     */
    if(!isset($_SESSION['chat_screenname']))
    {
        $_SESSION['chat_screenname'] = 'NewUser';
    }
    
    /*
     * Grab the message text from the relevant superglobal variable
     */
    $message = isset($_GET['message']) ? $_GET['message'] : '';
 
    /*
     * The message value must exist and not exceed 255 characters
     * User data must always be filtered and validated.
     * We allow empty messages, and assume an empty message
     * is a simple request to refresh the chat panel without
     * adding a new user message.
     */
    if(strlen($message) > 255)
    {
        throw new Exception('Invalid message! Must be 255 characters or less.');
    }
 
    /*
     * Create the XML file if not existing! Directory must be writeable...
     */
    if(!file_exists('./data/chat.xml'))
    {
        $newXML = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><chat></chat>';
        file_put_contents('./data/chat.xml', $newXML);
    }
 
    /*
     * Load the current XML store
     */
    $xml = simplexml_load_file('./data/chat.xml');
 
    /*
     * Only store the message if it's not empty!
     */
    if(!empty($message))
    {
        /*
         * First we entitise special characters so they do
         * not create future XML parsing errors. XML has several
         * invalid characters like <>&"' which can only be used
         * as entities, or when placed in a CDATA section.
         *
         * Although not fatal, SimpleXML will simply reverse
         * entities for quotes and single apostrophes before
         * writing the XML to file. The XML will therefore
         * be illegal, but will not create any problem for
         * SimpleXML when parsing. Odd behaviour...;)
         */
        $entity_message = htmlspecialchars($message, ENT_QUOTES);
 
        /*
         * Add new<p> <message> element to XML file
         */
        $newMessage = $xml->addChild('message');
 
        /*
         * Add our data to the new <message> element
         */
        $newMessage->addChild('author', $_SESSION['chat_screenname']);
        $newMessage->addChild('timestamp', time());
        $newMessage->addChild('text', $entity_message);
 
        /*
         * Write updated XML to file!
         */
        $xml->asXML('./data/chat.xml');
    }
 
    $phpMessageArray = $this->getNewMessages($xml);
 
    /*
     * Reset the user's Session chat_lrefresh value
     */
    $_SESSION['chat_lastrefresh'] = time();
 
    /*
     * Encode the PHP array into JSON
     */
    require_once 'Zend/Json.php';
    $responseJSON = Zend_Json::encode($phpMessageArray);
 
    /*
     * Echo the response for the user testing this
     * or for the AJAX handler on the client.
     */
    echo $responseJSON;
}

Notice that an original block of code has been replaced with a call to getNewMessages(). The code for this new private function is as follows:

private function getNewMessages($xml)
{
    /*
     * $xml already holds our XML data
     * The last refresh timestamp is stored in user's Session Data
     */
    $newMessages = $xml->xpath('/chat/message[timestamp>'
         . $_SESSION['chat_lastrefresh'] . ']');
    $newMessageCount = count($newMessages);
    $phpMessageArray = array();
 
    /*
     * Build a PHP array of new messages.
     *
     * We must cast each XML element to String (since it's only
     * done automatically if we attempt to use the element as a string
     * and PHP autmatically calls __toString() on a SimpleXMLElement
     * object, e.g. echo(). PHP 5.2.0+ will not require casting.
     *
     * Escape all expected text output based on user input for
     * htmlentities. (Security precaution.)
     */
    for($i=0;$i<$newMessageCount;++$i)
    {
        $phpMessageArray[$i]['author'] =
            htmlentities((string) $newMessages[$i]->author, ENT_QUOTES, 'utf-8');
        $phpMessageArray[$i]['timestamp'] =
            (string) $newMessages[$i]->timestamp;
        $phpMessageArray[$i]['text'] =
            htmlentities((string) $newMessages[$i]->text, ENT_QUOTES, 'utf-8');
    }
 
    return $phpMessageArray;
}

This does not change how our application operates. We're simply moving a section of code into a more specific method to make things in the original MessageAction() method easier to digest.

Figuring Out Who's Online

To select all online users, we add a new getOnlineUsers() method as follows:

private function getOnlineUsers($xml)
{
    /*
     * Our first step is the gather all author names from the xml
     * message> elements where the timestamp is within five minutes of
     * the current timestamp.
     * XPATH "/chat/message[timestamp>1161790302]/author"
     */
    $allOnlineScreenNames = $xml->xpath('/chat/message[timestamp>'
        . (time()-300) . ']/author');
 
    /*
     * Build a PHP array and escape names for html
     * entities. (Security precaution - this data will
     * be inserted into the HTML from our JSON reply.)
     */
    $uniqueOnlineScreenNames = array();
 
    /*
     * We only need one of each screen name! Only perform this
     * operation if a screen name exists and has been fetched from
     * the XML. Otherwise just return the empty unique array.
     */
    if(is_array($allOnlineScreenNames))
    {
        $allOnlineScreenNames = array_unique($allOnlineScreenNames);
        foreach($allOnlineScreenNames as $name)
        {
            $uniqueOnlineScreenNames[] =
                htmlentities((string) $name, ENT_QUOTES, 'utf-8');
        }
    }
 
    return $uniqueOnlineScreenNames;
}

The getOnlineUsers() method uses an XPATH query to grab all author names belonging to a element with a timestamp within the last 5 minutes. This will grab all the author names, so we use array_unique() to ensure we don't double up on names.

In the MessageAction() method we can then create a revised PHP array containing the new messages and updated user list as follows:

public function MessageAction()
{
    /*
     * Check for Session value chat_lastrefresh (last refresh
     * timestamp) otherwise set it to time() - 120 (2 minutes
     * earlier)
     *
     * The first refresh will therefore fetch all messages from
     * the previous 2 minutes.
     */
    if(!isset($_SESSION['chat_lastrefresh']))
    {
        $_SESSION['chat_lastrefresh'] = time() - 120;
    }
 
    /*
     * At this point the user has submitted a new chat message
     * without setting a screen name. Since the default is in
     * place, we'll simply add to the session for future use.
     */
    if(!isset($_SESSION['chat_screenname']))
    {
    $_SESSION['chat_screenname'] = 'NewUser';
    }
    
    /*
     * Grab the message text from the relevant superglobal variable
     */
    $message = isset($_GET['message']) ? $_GET['message'] : '';
 
    /*
     * The message value must exist and not exceed 255 characters
     * User data must always be filtered and validated.
     * We allow empty messages, and assume an empty message
     * is a simple request to refresh the chat panel without
     * adding a new user message.
     */
    if(strlen($message) > 255)
    {
    throw new Exception('Invalid message! Must be 255 characters or less.');
    }
 
    /*
     * Create the XML file if not existing! Directory must be writeable...
     */
    if(!file_exists('./data/chat.xml'))
    {
    $newXML ='<?xml version="1.0" encoding="UTF-8" standalone="yes"?><chat></chat>';
    file_put_contents('./data/chat.xml', $newXML);
    }
 
    /*
     * Load the current XML store
     */
    $xml = simplexml_load_file('./data/chat.xml');
 
    /*
     * Only store the message if it's not empty!
     */
    if(!empty($message))
    {
        /*
         * First we entitise special characters so they do
         * not create future XML parsing errors. XML has several
         * invalid characters like <>&"' which can only be used
         * as entities, or when placed in a CDATA section.
         *
         * Although not fatal, SimpleXML will simply reverse
         * entities for quotes and single apostrophes before
         * writing the XML to file. The XML will therefore
         * be illegal, but will not create any problem for
         * SimpleXML when parsing. Odd behaviour...;)
         */
        $entity_message = htmlspecialchars($message, ENT_QUOTES);
     
        /*
         * Add new <message> element to XML file
         */
        $newMessage = $xml->addChild('message');
     
        /*
         * Add our data to the new <message> element
         */
        $newMessage->addChild('author', $_SESSION['chat_screenname']);
        $newMessage->addChild('timestamp', time());
        $newMessage->addChild('text', $entity_message);
     
        /*
         * Write updated XML to file!
         */
        $xml->asXML('./data/chat.xml');
    }
 
    $phpMessageArray = $this->getNewMessages($xml);
    $onlineUsersArray = $this->getOnlineUsers($xml);
 
    /*
     * Reset the user's Session chat_lrefresh value
     */
    $_SESSION['chat_lastrefresh'] = time();
 
    /*
     * Encode the PHP array into JSON notation
     * We add both messages and online users to
     * different keys in the array.
     *
     * Note: Zend_Json has no issues with UTF-8;
     * the chat app will support multi-byte chars
     * with no issues (so long as your browser supports
     * them!).
     */
    require_once 'Zend/Json.php';
    $jsonArray = array(
        'newmessages'=>$phpMessageArray,
        'onlineusers'=>$onlineUsersArray
    );
    $responseJSON = Zend_Json::encode($jsonArray);
 
    /*
     * Set the Response for the user testing this
     * or for the AJAX handler on the client.
     */
    $this->getResponse()->setHeader('Content-Type', 'text/plain');
    $this->getResponse()->setBody($responseJSON);
}

We're almost there! In the revised MessageAction() method we're now encoding both the new messages and the current online users into our JSON response. The final step is to amend the Javascript handleRefresh() function in our client side chat.js file to pick up the online users and put them into the User List section of our application's HTML.

The revised handleRefresh() function follows.

function handleRefresh (reply)
{
    try
    {
        $('textmessage').value = '';
        var refreshResponse = eval('(' + reply.responseText + ')');
 
        /*
         * Handle New Messages
         */
        var newMessages = refreshResponse.newmessages;
        var messageCount = newMessages.length;
        for (var i=0; i<messageCount; i++)
        {
            $('chatpane').innerHTML = $('chatpane').innerHTML +
                '<p class="message"><span class="screenname">' + newMessages[i].author +
                ': </span>' + newMessages[i].text +
                '</p>';
        }
        $('chatpane').scrollTop = $('chatpane').scrollHeight;
 
        /*
         * Handle Updated Online Users
         */
        var onlineUsers = refreshResponse.onlineusers;
        var userCount = onlineUsers.length;
        $('userlist').innerHTML = '';
        for (var i=0; i<userCount; i++)
        {
            $('userlist').innerHTML = $('userlist').innerHTML +
                '<span class="screenname">' + onlineUsers[i] +
                '</span><br />';
        }
    }
    catch (e)
    {
        alert('Error: ' + e.toString());
    }
}

The main addition is the section for online users. First we empty the current list. Then we add a simple list of all the online users we were notified of from the server.

We've pretty much covered off on this application in its simple form. In the last part of this tutorial we will setup a timed interval for application to automatically refresh the application's data from the server without any user input. After that, we'll round up the tutorial and everyone can leave hopefully after learning something useful.

Comments


Wednesday, March 14, 2007
INTERESTING
12:47PM PDT · spilo101