<?php
/*
 * @package AJAX_Chat
 * @author Sebastian Tschan
 * @copyright (c) Sebastian Tschan
 * @license GNU Affero General Public License
 * @link https://blueimp.net/ajax/
 */

// Ajax Chat backend logic:
class AJAXChat {

    protected $db;
    protected $_config;
    protected $_requestVars;
    protected $_infoMessages;
    protected $_channels;
    protected $_allChannels;
    protected $_view;
    protected $_lang;
    protected $_invitations;
    protected $_customVars;
    protected $_sessionNew;
    protected $_onlineUsersData;
    protected $_bannedUsersData;

    function __construct() {
        // Initialize configuration settings:
        $this->initConfig();

        // Initialize the DataBase connection:
        $this->initDataBaseConnection();

        // Initialize request variables:
        $this->initRequestVars();

        // Initialize the chat session:
        $this->initSession();

        // Handle the browser request and send the response content:
        $this->handleRequest();
    }

    function initConfig() {
        $config = null;
        if(!include_once AJAX_CHAT_PATH . '/config/config.php') {
            print('<strong>Error:</strong> Configuration file could not be loaded.');
            exit;
        }
        $this->_config = &$config;

        // Initialize custom configuration settings:
        $this->initCustomConfig();
    }

    function initRequestVars() {
        $this->_requestVars = array();
        $this->_requestVars['ajax']         = isset($_REQUEST['ajax'])          ? true                          : false;
        $this->_requestVars['userID']       = isset($_REQUEST['userID'])        ? (int)$_REQUEST['userID']      : null;
        $this->_requestVars['userName']     = isset($_REQUEST['userName'])      ? $_REQUEST['userName']         : null;
        $this->_requestVars['channelID']    = isset($_REQUEST['channelID'])     ? (int)$_REQUEST['channelID']   : null;
        $this->_requestVars['channelName']  = isset($_REQUEST['channelName'])   ? $_REQUEST['channelName']      : null;
        $this->_requestVars['text']         = isset($_POST['text'])             ? $_POST['text']                : null;
        $this->_requestVars['lastID']       = isset($_REQUEST['lastID'])        ? (int)$_REQUEST['lastID']      : 0;
        $this->_requestVars['login']        = isset($_REQUEST['login'])         ? true                          : false;
        $this->_requestVars['logout']       = isset($_POST['logout'])           ? true                          : false;
        $this->_requestVars['password']     = isset($_REQUEST['password'])      ? $_REQUEST['password']         : null;
        $this->_requestVars['view']         = isset($_REQUEST['view'])          ? $_REQUEST['view']             : null;
        $this->_requestVars['year']         = isset($_REQUEST['year'])          ? (int)$_REQUEST['year']        : null;
        $this->_requestVars['month']        = isset($_REQUEST['month'])         ? (int)$_REQUEST['month']       : null;
        $this->_requestVars['day']          = isset($_REQUEST['day'])           ? (int)$_REQUEST['day']         : null;
        $this->_requestVars['hour']         = isset($_REQUEST['hour'])          ? (int)$_REQUEST['hour']        : null;
        $this->_requestVars['search']       = isset($_REQUEST['search'])        ? $_REQUEST['search']           : null;
        $this->_requestVars['shoutbox']     = isset($_REQUEST['shoutbox'])      ? true                          : false;
        $this->_requestVars['getInfos']     = isset($_REQUEST['getInfos'])      ? $_REQUEST['getInfos']         : null;
        $this->_requestVars['lang']         = isset($_REQUEST['lang'])          ? $_REQUEST['lang']             : null;
        $this->_requestVars['delete']       = isset($_REQUEST['delete'])        ? (int)$_REQUEST['delete']      : null;

        // Initialize custom request variables:
        $this->initCustomRequestVars();

        // Remove slashes which have been added to user input strings if magic_quotes_gpc is On:
        /*if(get_magic_quotes_gpc()) {
            // It is safe to remove the slashes as we escape user data ourself
            array_walk(
                $this->_requestVars,
                create_function(
                    '&$value, $key',
                    'if(is_string($value)) $value = stripslashes($value);'
                )
            );
        }*/
    }

    function initDataBaseConnection() {
        // Create a new database object:
        $this->db = new AJAXChatDataBase(
            $this->_config['dbConnection']
        );
        // Use a new database connection if no existing is given:
        if(!$this->_config['dbConnection']['link']) {
            // Connect to the database server:
            $this->db->connect($this->_config['dbConnection']);
            if($this->db->error()) {
                echo $this->db->getError();
                die();
            }
            // Select the database:
            $this->db->select($this->_config['dbConnection']['name']);
            if($this->db->error()) {
                echo $this->db->getError();
                die();
            }
        }
        // Unset the dbConnection array for safety purposes:
        unset($this->_config['dbConnection']);
    }

    function getDataBaseTable($table) {
        return ($this->db->getName() ? $this->db->getName().'.'.$this->getConfig('dbTableNames',$table) : $this->getConfig('dbTableNames',$table));
    }

    function initSession() {
        // Start the PHP session (if not already started):
        $this->startSession();

        if($this->isLoggedIn()) {
            // Logout if we receive a logout request, the chat has been closed or the userID could not be revalidated:
            if($this->getRequestVar('logout') || !$this->isChatOpen() || !$this->revalidateUserID()) {
                $this->logout();
                return;
            }
            // Logout if the Session IP is not the same when logged in and ipCheck is enabled:
            if($this->getConfig('ipCheck') && ($this->getSessionIP() === null || $this->getSessionIP() != $_SERVER['REMOTE_ADDR'])) {
                $this->logout('IP');
                return;
            }
        } else if(
            // Login if auto-login enabled or a login, userName or shoutbox parameter is given:
            $this->getConfig('forceAutoLogin') ||
            $this->getRequestVar('login') ||
            $this->getRequestVar('userName') ||
            $this->getRequestVar('shoutbox')
            ) {
            $this->login();
        }

        // Initialize the view:
        $this->initView();

        if($this->getView() == 'chat') {
            $this->initChatViewSession();
        } else if($this->getView() == 'logs') {
            $this->initLogsViewSession();
        }

        if(!$this->getRequestVar('ajax') && !headers_sent()) {
            // Set style cookie:
            $this->setStyle();
            // Set langCode cookie:
            $this->setLangCodeCookie();
        }

        $this->initCustomSession();
    }

    function initLogsViewSession() {
        if($this->getConfig('socketServerEnabled')) {
            if(!$this->getSessionVar('logsViewSocketAuthenticated')) {
                $this->updateLogsViewSocketAuthentication();
                $this->setSessionVar('logsViewSocketAuthenticated', true);
            }
        }
    }

    function updateLogsViewSocketAuthentication() {
        if($this->getUserRole() != AJAX_CHAT_ADMIN) {
            $channels = array();
            foreach($this->getChannels() as $channel) {
                if($this->getConfig('logsUserAccessChannelList') && !in_array($channel, $this->getConfig('logsUserAccessChannelList'))) {
                    continue;
                }
                array_push($channels, $channel);
            }
            array_push($channels, $this->getPrivateMessageID());
            array_push($channels, $this->getPrivateChannelID());
        } else {
            // The channelID "ALL" authenticates for all channels:
            $channels = array('ALL');
        }
        $this->updateSocketAuthentication(
            $this->getUserID(),
            $this->getSocketRegistrationID(),
            $channels
        );
    }

    function initChatViewSession() {
        // If channel is not null we are logged in to the chat view:
        if($this->getChannel() !== null) {
            // Check if the current user has been logged out due to inactivity:
            if(!$this->isUserOnline()) {
                $this->logout();
                return;
            }
            if($this->getRequestVar('ajax')) {
                $this->initChannel();
                $this->updateOnlineStatus();
                $this->checkAndRemoveInactive();
            }
        } else {
            if($this->getRequestVar('ajax')) {
                // Set channel, insert login messages and add to online list on first ajax request in chat view:
                $this->chatViewLogin();
            }
        }
    }

    function isChatOpen() {
        if($this->getUserRole() == AJAX_CHAT_ADMIN)
            return true;
        if($this->getConfig('chatClosed'))
            return false;
        $time = time();
        if($this->getConfig('timeZoneOffset') !== null) {
            // Subtract the server timezone offset and add the config timezone offset:
            $time -= date('Z', $time);
            $time += $this->getConfig('timeZoneOffset');
        }
        // Check the opening hours:
        if($this->getConfig('openingHour') < $this->getConfig('closingHour'))
        {
            if(($this->getConfig('openingHour') > date('G', $time)) || ($this->getConfig('closingHour') <= date('G', $time)))
                return false;
        }
        else
        {
            if(($this->getConfig('openingHour') > date('G', $time)) && ($this->getConfig('closingHour') <= date('G', $time)))
                return false;
        }
        // Check the opening weekdays:
        if(!in_array(date('w', $time), $this->getConfig('openingWeekDays')))
            return false;
        return true;
    }

    function handleRequest() {
        if($this->getRequestVar('ajax')) {
            if($this->isLoggedIn()) {
                // Parse info requests (for current userName, etc.):
                $this->parseInfoRequests();

                // Parse command requests (e.g. message deletion):
                $this->parseCommandRequests();

                // Parse message requests:
                $this->initMessageHandling();
            }
            // Send chat messages and online user list in XML format:
            $this->sendXMLMessages();
        } else {
            // Display XHTML content for non-ajax requests:
            $this->sendXHTMLContent();
        }
    }

    function parseCommandRequests() {
        if($this->getRequestVar('delete') !== null) {
            $this->deleteMessage($this->getRequestVar('delete'));
        }
    }

    function parseInfoRequests() {
        if($this->getRequestVar('getInfos')) {
            $infoRequests = explode(',', $this->getRequestVar('getInfos'));
            foreach($infoRequests as $infoRequest) {
                $this->parseInfoRequest($infoRequest);
            }
        }
    }

    function parseInfoRequest($infoRequest) {
        switch($infoRequest) {
            case 'userID':
                $this->addInfoMessage($this->getUserID(), 'userID');
                break;
            case 'userName':
                $this->addInfoMessage($this->getUserName(), 'userName');
                break;
            case 'userRole':
                $this->addInfoMessage($this->getUserRole(), 'userRole');
                break;
            case 'channelID':
                $this->addInfoMessage($this->getChannel(), 'channelID');
                break;
            case 'channelName':
                $this->addInfoMessage($this->getChannelName(), 'channelName');
                break;
            case 'socketRegistrationID':
                $this->addInfoMessage($this->getSocketRegistrationID(), 'socketRegistrationID');
                break;
            default:
                $this->parseCustomInfoRequest($infoRequest);
        }
    }

    function sendXHTMLContent() {
        $httpHeader = new AJAXChatHTTPHeader($this->getConfig('contentEncoding'), $this->getConfig('contentType'));

        $template = new AJAXChatTemplate($this, $this->getTemplateFileName(), $httpHeader->getContentType());

        // Send HTTP header:
        $httpHeader->send();

        // Send parsed template content:
        echo $template->getParsedContent();
    }

    function getTemplateFileName() {
        switch($this->getView()) {
            case 'chat':
                return AJAX_CHAT_PATH . '/template/loggedIn.html';
            case 'logs':
                return AJAX_CHAT_PATH . '/template/logs.html';
            case 'mobile':
                return AJAX_CHAT_PATH . '/template/mobile.html';
            case 'legacy':
                return AJAX_CHAT_PATH . '/template/legacyLogin.html';
            case 'banned':
                return AJAX_CHAT_PATH . '/template/banned.html';
            case 'legacy1':
                return AJAX_CHAT_PATH . '/template/~loggedOut.html';
            case 'legacy2':
                return AJAX_CHAT_PATH . '/template/loggedOut~.html';
            case 'legacy3':
                return AJAX_CHAT_PATH . '/template/loggedOut.html';
            default:
                return AJAX_CHAT_PATH . '/template/loggedOutFA.html';
        }
    }

    function initView() {
        $this->_view = null;
        // "chat" is the default view:
        $view = ($this->getRequestVar('view') === null) ? 'chat' : $this->getRequestVar('view');
        if($this->hasAccessTo($view)) {
            $this->_view = $view;
        }
    }

    function getView() {
        return $this->_view;
    }

    function hasAccessTo($view) {
        if(substr($view, 0, 6) === 'legacy')
            return !$this->isLoggedIn();

        switch($view) {
            case 'legacy':
                if($this->isLoggedIn()) {
                    return false;
                }
                return true;
            case 'banned':
                return true;
            case 'chat':
            case 'mobile':
            case 'teaser':
                if($this->isLoggedIn()) {
                    return true;
                }
                return false;
            case 'logs':
                if($this->isLoggedIn() && ($this->getUserRole() == AJAX_CHAT_ADMIN || $this->getUserRole() == CMOD || $this->getUserRole() == AJAX_CHAT_MODERATOR || $this->getUserRole() == PURPLE || $this->getUserRole() == DONATOR ||
                    ($this->getConfig('logsUserAccess') &&
                    ($this->getUserRole() == AJAX_CHAT_USER))
                    )) {
                    return true;
                }
                return false;
            default:
                return false;
        }
    }

    function login() {
        // Retrieve valid login user data (from request variables or session data):
        $userData = $this->getValidLoginUserData();

        if(!$userData) {
            $this->addInfoMessage('errorInvalidUser');
            return false;
        }

        // If the chat is closed, only the admin may login:
        if(!$this->isChatOpen() && $userData['userRole'] != AJAX_CHAT_ADMIN) {
            $this->addInfoMessage('errorChatClosed');
            return false;
        }

        if(!$this->getConfig('allowGuestLogins') && $userData['userRole'] == AJAX_CHAT_GUEST) {
            return false;
        }

        // Check if userID or userName are already listed online:
        if($this->isUserOnline($userData['userID']) || $this->isUserNameInUse($userData['userName'])) {
            // Set the registered user inactive and remove the inactive users so the user can be logged in again:
            $this->setInactive($userData['userID'], $userData['userName']);
            $this->removeInactive();
        }

        // Check if user is banned:
        if($userData['userRole'] != AJAX_CHAT_ADMIN && $this->isUserBanned($userData['userName'], $userData['userID'], $_SERVER['REMOTE_ADDR'])) {
            $this->addInfoMessage('errorBanned');
            return false;
        }

        // Check if the max number of users is logged in (not affecting moderators or admins):
        if(!($userData['userRole'] == AJAX_CHAT_MODERATOR || $userData['userRole'] == PURPLE || $userData['userRole'] == AJAX_CHAT_ADMIN) && $this->isMaxUsersLoggedIn()) {
            $this->addInfoMessage('errorMaxUsersLoggedIn');
            return false;
        }

        // Use a new session id (if session has been started by the chat):
        $this->regenerateSessionID();

        // Log in:
        $this->setUserID($userData['userID']);
        $this->setUserName($userData['userName']);
        $this->setLoginUserName($userData['userName']);
        $this->setUserRole($userData['userRole']);
        $this->setLoggedIn(true);
        $this->setLoginTimeStamp(time());

        // IP Security check variable:
        $this->setSessionIP($_SERVER['REMOTE_ADDR']);

        // The client authenticates to the socket server using a socketRegistrationID:
        if($this->getConfig('socketServerEnabled')) {
            $this->setSocketRegistrationID(
                md5(uniqid(rand(), true))
            );
        }

        // Add userID, userName and userRole to info messages:
        $this->addInfoMessage($this->getUserID(), 'userID');
        $this->addInfoMessage($this->getUserName(), 'userName');
        $this->addInfoMessage($this->getUserRole(), 'userRole');

        // Purge logs:
        if($this->getConfig('logsPurgeLogs')) {
            $this->purgeLogs();
        }

        // Report login to Satori:
        /*$boatcom = @fsockopen('marie.railgun.sh', 9064, $errno, $errstr, 5);
        if($boatcom) {
            $message = sprintf('{sock}[i][url=https://flashii.net/profile.php?u=%d][b]%s[/b][/url] logged into [url=https://flash.moe/chat/]Flashii Chat Legacy[/url].[/i]', $userData['userID'], $userData['userName']);
            $message = chr(0xF) . hash_hmac('sha256', $message, 'lol glad i caught this, that could\'ve been pretty funny if i forgot to remove this') . $message . chr(0xF);
            fwrite($boatcom, $message);
            fflush($boatcom);
            fclose($boatcom);
        }*/

        return true;
    }

    function chatViewLogin() {
        $this->setChannel($this->getValidRequestChannelID());
        $this->addToOnlineList();

        // Add channelID and channelName to info messages:
        $this->addInfoMessage($this->getChannel(), 'channelID');
        $this->addInfoMessage($this->getChannelName(), 'channelName');

        // Login message:
        $text = '/login '.$this->getUserName();
        $this->insertChatBotMessage(
            $this->getChannel(),
            $text,
            null,
            1
        );
    }

    function getValidRequestChannelID() {
        $channelID = $this->getRequestVar('channelID');
        $channelName = $this->getRequestVar('channelName');
        // Check the given channelID, or get channelID from channelName:
        if($channelID === null) {
            if($channelName !== null) {
                $channelID = $this->getChannelIDFromChannelName($channelName);
                // channelName might need encoding conversion:
                if($channelID === null) {
                    $channelID = $this->getChannelIDFromChannelName(
                                    $this->trimChannelName($channelName, $this->getConfig('contentEncoding'))
                                );
                }
            }
        }
        // Validate the resulting channelID:
        if(!$this->validateChannel($channelID)) {
            if($this->getChannel() !== null) {
                return $this->getChannel();
            }
            return $this->getConfig('defaultChannelID');
        }
        return $channelID;
    }

    function initChannel() {
        $channelID = $this->getRequestVar('channelID');
        $channelName = $this->getRequestVar('channelName');
        if($channelID !== null) {
            $this->switchChannel($this->getChannelNameFromChannelID($channelID));
        } else if($channelName !== null) {
            if($this->getChannelIDFromChannelName($channelName) === null) {
                // channelName might need encoding conversion:
                $channelName = $this->trimChannelName($channelName, $this->getConfig('contentEncoding'));
            }
            $this->switchChannel($channelName);
        }
    }

    function logout($type=null) {
        // Update the socket server authentication for the user:
        if($this->getConfig('socketServerEnabled')) {
            $this->updateSocketAuthentication($this->getUserID());
        }
        if($this->isUserOnline()) {
            $this->chatViewLogout($type);
        }
        $this->setLoggedIn(false);
        $this->destroySession();

        // Re-initialize the view:
        $this->initView();
    }

    function chatViewLogout($type) {
        $this->removeFromOnlineList();
        if($type !== null) {
            $type = ' '.$type;
        }
        // Logout message
        $text = '/logout '.$this->getUserName().$type;
        $this->insertChatBotMessage(
            $this->getChannel(),
            $text,
            null,
            1
        );
    }

    function switchChannel($channelName) {
        $channelID = $this->getChannelIDFromChannelName($channelName);

        if($channelID !== null && $this->getChannel() == $channelID) {
            // User is already in the given channel, return:
            return;
        }

        // Check if we have a valid channel:
        if(!$this->validateChannel($channelID)) {
            // Invalid channel:
            $text = '/error InvalidChannelName '.$channelName;
            $this->insertChatBotMessage(
                $this->getPrivateMessageID(),
                $text
            );
            return;
        }

        $oldChannel = $this->getChannel();

        $this->setChannel($channelID);
        $this->updateOnlineList();

        // Channel leave message
        $text = '/channelLeave '.$this->getUserName();
        $this->insertChatBotMessage(
            $oldChannel,
            $text,
            null,
            1
        );

        // Channel enter message
        $text = '/channelEnter '.$this->getUserName();
        $this->insertChatBotMessage(
            $this->getChannel(),
            $text,
            null,
            1
        );

        $this->addInfoMessage($channelName, 'channelSwitch');
        $this->addInfoMessage($channelID, 'channelID');
        $this->_requestVars['lastID'] = 0;
    }

    function addToOnlineList() {
        $sql = 'INSERT INTO '.$this->getDataBaseTable('online').'(
                    userID,
                    userName,
                    userRole,
                    channel,
                    dateTime,
                    ip
                )
                VALUES (
                    '.$this->db->makeSafe($this->getUserID()).',
                    '.$this->db->makeSafe($this->getUserName()).',
                    '.$this->db->makeSafe($this->getUserRole()).',
                    '.$this->db->makeSafe($this->getChannel()).',
                    NOW(),
                    '.$this->db->makeSafe($this->ipToStorageFormat($_SERVER['REMOTE_ADDR'])).'
                );';

        // Create a new SQL query:
        $result = $this->db->sqlQuery($sql);

        // Stop if an error occurs:
        if($result->error()) {
            echo $result->getError();
            die();
        }

        $this->resetOnlineUsersData();
    }

    function removeFromOnlineList() {
        $sql = 'DELETE FROM
                    '.$this->getDataBaseTable('online').'
                WHERE
                    userID = '.$this->db->makeSafe($this->getUserID()).';';

        // Create a new SQL query:
        $result = $this->db->sqlQuery($sql);

        // Stop if an error occurs:
        if($result->error()) {
            echo $result->getError();
            die();
        }

        $this->removeUserFromOnlineUsersData();
    }

    function updateOnlineList() {
        $sql = 'UPDATE
                    '.$this->getDataBaseTable('online').'
                SET
                    userName    = '.$this->db->makeSafe($this->getUserName()).',
                    channel     = '.$this->db->makeSafe($this->getChannel()).',
                    dateTime    = NOW(),
                    ip          = '.$this->db->makeSafe($this->ipToStorageFormat($_SERVER['REMOTE_ADDR'])).'
                WHERE
                    userID = '.$this->db->makeSafe($this->getUserID()).';';

        // Create a new SQL query:
        $result = $this->db->sqlQuery($sql);

        // Stop if an error occurs:
        if($result->error()) {
            echo $result->getError();
            die();
        }

        $this->resetOnlineUsersData();
    }

    function initMessageHandling() {
        // Don't handle messages if we are not in chat view:
        if($this->getView() != 'chat') {
            return;
        }

        // Check if we have been uninvited from a private or restricted channel:
        if(!$this->validateChannel($this->getChannel())) {
            // Switch to the default channel:
            $this->switchChannel($this->getChannelNameFromChannelID($this->getConfig('defaultChannelID')));
            return;
        }

        if($this->getRequestVar('text') !== null) {
            $this->insertMessage($this->getRequestVar('text'));
        }
    }

    function insertParsedMessage($text) {
        // Replace Unicode character
        $text = preg_replace("/\xEF\xBF\xBF/im", " ", $text);

        $gtParse = preg_replace('/\\[(?:\\/)?(\\w+)(?:=([^<>]*?))?\\]/ms', '', $text);

        // Auto greentext
        if(preg_match("/>(.*?)$/im", $gtParse)) {
            $text = preg_replace("/>(.*?)$/im", "[color=Green]>$1[/color]", $gtParse);
        }
        if(preg_match("/(.*?)<$/im", $gtParse)) {
            $text = preg_replace("/(.*?)<$/im", "[color=Red]$1<[/color]", $gtParse);
        }

        // Emoticon Limiting
        $emoticons = array(':happy:', ':lmao:', ':angry:', ':angrier:', ':evil:', ':glare:', ':eat:', ':lol:', ':dizzy:', ':yay:', ':wtf:', ':sigh:', ':omg:', ':ouch:', ':tired:', ':kiss:', ':love:', ':sweat:', ':suspicious:', ':crying:', ':blank:', ':puke:', ':ruse:', ':meow:');
        $emoticonCount = 0;

        foreach($emoticons as $emoticon) {
            // Increase Emoticon count
            $emoticonCount += substr_count($text, $emoticon);

            // If higher than 10 replace then
            if($emoticonCount > 10)
                $text = str_replace($emoticon, '', $text);
        }

        if($this->getQueryUserName() !== null && strpos($text, '/') !== 0) {
            // If a queryUserName is set, sent all messages as private messages to this userName:
            $text = '/msg '.$this->getQueryUserName().' '.$text;
        }

        if(strpos($text, '/') === 0) {
            // Parse IRC-style commands:
            $textParts = explode(' ', $text);

            switch($textParts[0]) {
                // Channel switch:
                case '/join':
                    $this->insertParsedMessageJoin($textParts);
                    break;

                // Logout:
                case '/quit':
                case '/suicide':
                    $this->logout();
                    break;

                // Private message:
                case '/msg':
                case '/describe':
                    $this->insertParsedMessagePrivMsg($textParts);
                    break;

                // Invitation:
                case '/invite':
                    $this->insertParsedMessageInvite($textParts);
                    break;

                // Uninvitation:
                case '/uninvite':
                    $this->insertParsedMessageUninvite($textParts);
                    break;

                // Private messaging:
                case '/query':
                    $this->insertParsedMessageQuery($textParts);
                    break;

                // Kicking offending users from the chat:
                case '/kick':
                    $this->insertParsedMessageKick($textParts);
                    break;

                // Listing banned users:
                case '/bans':
                    $this->insertParsedMessageBans($textParts);
                    break;

                // Unban user (remove from ban list):
                case '/unban':
                    $this->insertParsedMessageUnban($textParts);
                    break;

                // Describing actions:
                case '/me':
                case '/action':
                    $this->insertParsedMessageAction($textParts);
                    break;

                // Listing online Users:
                case '/who':
                    $this->insertParsedMessageWho($textParts);
                    break;

                // Listing available channels:
                case '/list':
                    $this->insertParsedMessageList($textParts);
                    break;

                // Retrieving the channel of a User:
                case '/whereis':
                    $this->insertParsedMessageWhereis($textParts);
                    break;

                // Listing information about a User:
                case '/whois':
                    $this->insertParsedMessageWhois($textParts);
                    break;

                // Rolling dice:
                case '/roll':
                    $this->insertParsedMessageRoll($textParts);
                    break;

                // Switching userName:
                case '/nick':
                    $this->insertParsedMessageNick($textParts);
                    break;

                // Switching userName:
                case '/saibatekuisagoodsite':
                    $this->insertParsedMessageChangeNick($textParts);
                    break;

                // Custom or unknown command:
                default:
                    if(!$this->parseCustomCommands($text, $textParts)) {
                        $this->insertChatBotMessage(
                            $this->getPrivateMessageID(),
                            '/error UnknownCommand '.$textParts[0]
                        );
                    }
            }

        } else {
            // No command found, just insert the plain message:
            $this->insertCustomMessage(
                $this->getUserID(),
                $this->getUserName(),
                $this->getUserRole(),
                $this->getChannel(),
                $text
            );
        }
        include_once 'satori.php';
    }

    function insertParsedMessageJoin($textParts) {
        if(count($textParts) == 1) {
            // join with no arguments is the own private channel, if allowed:
            if($this->isAllowedToCreatePrivateChannel()) {
                // Private channels are identified by square brackets:
                $this->switchChannel($this->getChannelNameFromChannelID($this->getPrivateChannelID()));
            } else {
                $this->insertChatBotMessage(
                    $this->getPrivateMessageID(),
                    '/error MissingChannelName'
                );
            }
        } else {
            $this->switchChannel($textParts[1]);
        }
    }

    function insertParsedMessagePrivMsg($textParts) {
        if($this->isAllowedToSendPrivateMessage()) {
            if(count($textParts) < 3) {
                if(count($textParts) == 2) {
                    $this->insertChatBotMessage(
                        $this->getPrivateMessageID(),
                        '/error MissingText'
                    );
                } else {
                    $this->insertChatBotMessage(
                        $this->getPrivateMessageID(),
                        '/error MissingUserName'
                    );
                }
            } else {
                // Get UserID from UserName:
                $toUserID = $this->getIDFromName($textParts[1]);
                if($toUserID === null) {
                    if($this->getQueryUserName() !== null) {
                        // Close the current query:
                        $this->insertMessage('/query');
                    } else {
                        $this->insertChatBotMessage(
                            $this->getPrivateMessageID(),
                            '/error UserNameNotFound '.$textParts[1]
                        );
                    }
                } else {
                    // Insert /privaction command if /describe is used:
                    $command = ($textParts[0] == '/describe') ? '/privaction' : '/privmsg';
                    // Copy of private message to current User:
                    $this->insertCustomMessage(
                        $this->getUserID(),
                        $this->getUserName(),
                        $this->getUserRole(),
                        $this->getPrivateMessageID(),
                        $command.'to '.$textParts[1].' '.implode(' ', array_slice($textParts, 2))
                    );
                    // Private message to requested User:
                    $this->insertCustomMessage(
                        $this->getUserID(),
                        $this->getUserName(),
                        $this->getUserRole(),
                        $this->getPrivateMessageID($toUserID),
                        $command.' '.implode(' ', array_slice($textParts, 2))
                    );
                }
            }
        } else {
            $this->insertChatBotMessage(
                $this->getPrivateMessageID(),
                '/error PrivateMessageNotAllowed'
            );
        }
    }

    function insertParsedMessageInvite($textParts) {
        if($this->getChannel() == $this->getPrivateChannelID() || in_array($this->getChannel(), $this->getChannels())) {
            if(count($textParts) == 1) {
                $this->insertChatBotMessage(
                    $this->getPrivateMessageID(),
                    '/error MissingUserName'
                );
            } else {
                $toUserID = $this->getIDFromName($textParts[1]);
                if($toUserID === null) {
                    $this->insertChatBotMessage(
                        $this->getPrivateMessageID(),
                        '/error UserNameNotFound '.$textParts[1]
                    );
                } else {
                    // Add the invitation to the database:
                    $this->addInvitation($toUserID);
                    $invitationChannelName = $this->getChannelNameFromChannelID($this->getChannel());
                    // Copy of invitation to current User:
                    $this->insertChatBotMessage(
                        $this->getPrivateMessageID(),
                        '/inviteto '.$textParts[1].' '.$invitationChannelName
                    );
                    // Invitation to requested User:
                    $this->insertChatBotMessage(
                        $this->getPrivateMessageID($toUserID),
                        '/invite '.$this->getUserName().' '.$invitationChannelName
                    );
                }
            }
        } else {
            $this->insertChatBotMessage(
                $this->getPrivateMessageID(),
                '/error InviteNotAllowed'
            );
        }
    }

    function insertParsedMessageUninvite($textParts) {
        if($this->getChannel() == $this->getPrivateChannelID() || in_array($this->getChannel(), $this->getChannels())) {
            if(count($textParts) == 1) {
                $this->insertChatBotMessage(
                    $this->getPrivateMessageID(),
                    '/error MissingUserName'
                );
            } else {
                $toUserID = $this->getIDFromName($textParts[1]);
                if($toUserID === null) {
                    $this->insertChatBotMessage(
                        $this->getPrivateMessageID(),
                        '/error UserNameNotFound '.$textParts[1]
                    );
                } else {
                    // Remove the invitation from the database:
                    $this->removeInvitation($toUserID);
                    $invitationChannelName = $this->getChannelNameFromChannelID($this->getChannel());
                    // Copy of uninvitation to current User:
                    $this->insertChatBotMessage(
                        $this->getPrivateMessageID(),
                        '/uninviteto '.$textParts[1].' '.$invitationChannelName
                    );
                    // Uninvitation to requested User:
                    $this->insertChatBotMessage(
                        $this->getPrivateMessageID($toUserID),
                        '/uninvite '.$this->getUserName().' '.$invitationChannelName
                    );
                }
            }
        } else {
            $this->insertChatBotMessage(
                $this->getPrivateMessageID(),
                '/error UninviteNotAllowed'
            );
        }
    }

    function insertParsedMessageQuery($textParts) {
        if($this->isAllowedToSendPrivateMessage()) {
            if(count($textParts) == 1) {
                if($this->getQueryUserName() !== null) {
                    $this->insertChatBotMessage(
                        $this->getPrivateMessageID(),
                        '/queryClose '.$this->getQueryUserName()
                    );
                    // Close the current query:
                    $this->setQueryUserName(null);
                } else {
                    $this->insertChatBotMessage(
                        $this->getPrivateMessageID(),
                        '/error NoOpenQuery'
                    );
                }
            } else {
                if($this->getIDFromName($textParts[1]) === null) {
                    $this->insertChatBotMessage(
                        $this->getPrivateMessageID(),
                        '/error UserNameNotFound '.$textParts[1]
                    );
                } else {
                    if($this->getQueryUserName() !== null) {
                        // Close the current query:
                        $this->insertMessage('/query');
                    }
                    // Open a query to the requested user:
                    $this->setQueryUserName($textParts[1]);
                    $this->insertChatBotMessage(
                        $this->getPrivateMessageID(),
                        '/queryOpen '.$textParts[1]
                    );
                }
            }
        } else {
            $this->insertChatBotMessage(
                $this->getPrivateMessageID(),
                '/error PrivateMessageNotAllowed'
            );
        }
    }

    function insertParsedMessageKick($textParts) {
        // Only moderators/admins may kick users:
        if($this->getUserRole() == AJAX_CHAT_ADMIN || $this->getUserRole() == AJAX_CHAT_MODERATOR || $this->getUserRole() == CMOD || $this->getUserID() == 11) {
            if(count($textParts) == 1) {
                $this->insertChatBotMessage(
                    $this->getPrivateMessageID(),
                    '/error MissingUserName'
                );
            } else {
                // Get UserID from UserName:
                $kickUserID = $this->getIDFromName($textParts[1]);
                if($kickUserID === null) {
                    $this->insertChatBotMessage(
                        $this->getPrivateMessageID(),
                        '/error UserNameNotFound '.$textParts[1]
                    );
                } else {
                    // Check the role of the user to kick:
                    $kickUserRole = $this->getRoleFromID($kickUserID);
                    if(
                        $kickUserRole == AJAX_CHAT_ADMIN ||
                            (
                                (
                                    $kickUserID == 11
                                    &&
                                    $this->getUserRole() != AJAX_CHAT_ADMIN
                                    &&
                                    $this->getUserID() != 11
                                )
                            ||
                                (
                                    $kickUserRole == CMOD
                                    &&
                                    $this->getUserRole() != AJAX_CHAT_MODERATOR
                                    &&
                                    $this->getUserRole() != PURPLE
                                    &&
                                    $this->getUserRole() != AJAX_CHAT_ADMIN
                                    &&
                                    $this->getUserID() != 11
                                )
                            ||
                                (
                                    $kickUserRole == AJAX_CHAT_MODERATOR
                                    &&
                                    $this->getUserRole() != PURPLE
                                    &&
                                    $this->getUserRole() != AJAX_CHAT_ADMIN
                                    &&
                                    $this->getUserID() != 11
                                )
                            )
                    ) {
                        // Admins and moderators may not be kicked:
                        $this->insertChatBotMessage(
                            $this->getPrivateMessageID(),
                            '/error KickNotAllowed '.$textParts[1]
                        );
                    } else {
                        // Kick user and insert message:
                        $channel = $this->getChannelFromID($kickUserID);
                        $banMinutes = (count($textParts) > 2) ? $textParts[2] : null;
                        $this->kickUser($textParts[1], $banMinutes, $kickUserID);
                        // If no channel found, user logged out before he could be kicked
                        if($channel !== null) {
                            $this->insertChatBotMessage(
                                $channel,
                                '/kick '.$textParts[1],
                                null,
                                1
                            );
                            // Send a copy of the message to the current user, if not in the channel:
                            if($channel != $this->getChannel()) {
                                $this->insertChatBotMessage(
                                    $this->getPrivateMessageID(),
                                    '/kick '.$textParts[1],
                                    null,
                                    1
                                );
                            }
                        }
                    }
                }
            }
        } else {
            $this->insertChatBotMessage(
                $this->getPrivateMessageID(),
                '/error CommandNotAllowed '.$textParts[0]
            );
        }
    }

    function insertParsedMessageBans($textParts) {
        // Only moderators/admins may see the list of banned users:
        if($this->getUserRole() == AJAX_CHAT_ADMIN || $this->getUserRole() == AJAX_CHAT_MODERATOR || $this->getUserRole() == CMOD) {
            $this->removeExpiredBans();
            $bannedUsers = $this->getBannedUsers();
            if(count($bannedUsers) > 0) {
                $this->insertChatBotMessage(
                    $this->getPrivateMessageID(),
                    '/bans '.implode(' ', $bannedUsers)
                );
            } else {
                $this->insertChatBotMessage(
                    $this->getPrivateMessageID(),
                    '/bansEmpty -'
                );
            }
        } else {
            $this->insertChatBotMessage(
                $this->getPrivateMessageID(),
                '/error CommandNotAllowed '.$textParts[0]
            );
        }
    }

    function insertParsedMessageUnban($textParts) {
        // Only moderators/admins may unban users:
        if($this->getUserRole() == AJAX_CHAT_ADMIN || $this->getUserRole() == AJAX_CHAT_MODERATOR || $this->getUserRole() == CMOD) {
            $this->removeExpiredBans();
            if(count($textParts) == 1) {
                $this->insertChatBotMessage(
                    $this->getPrivateMessageID(),
                    '/error MissingUserName'
                );
            } else {
                if(!in_array($textParts[1], $this->getBannedUsers())) {
                    $this->insertChatBotMessage(
                        $this->getPrivateMessageID(),
                        '/error UserNameNotFound '.$textParts[1]
                    );
                } else {
                    // Unban user and insert message:
                    $this->unbanUser($textParts[1]);
                    $this->insertChatBotMessage(
                        $this->getPrivateMessageID(),
                        '/unban '.$textParts[1]
                    );
                }
            }
        } else {
            $this->insertChatBotMessage(
                $this->getPrivateMessageID(),
                '/error CommandNotAllowed '.$textParts[0]
            );
        }
    }

    function insertParsedMessageAction($textParts) {
        if(count($textParts) == 1) {
            $this->insertChatBotMessage(
                $this->getPrivateMessageID(),
                '/error MissingText'
            );
        } else {
            if($this->getQueryUserName() !== null) {
                // If we are in query mode, sent the action to the query user:
                $this->insertMessage('/describe '.$this->getQueryUserName().' '.implode(' ', array_slice($textParts, 1)));
            } else {
                $this->insertCustomMessage(
                    $this->getUserID(),
                    $this->getUserName(),
                    $this->getUserRole(),
                    $this->getChannel(),
                    implode(' ', $textParts)
                );
            }
        }
    }

    function insertParsedMessageWho($textParts) {
        if(count($textParts) == 1) {
            if($this->isAllowedToListHiddenUsers()) {
                // List online users from any channel:
                $this->insertChatBotMessage(
                    $this->getPrivateMessageID(),
                    '/who '.implode(' ', $this->getOnlineUsers())
                );
            } else {
                // Get online users for all accessible channels:
                $channels = $this->getChannels();
                // Add the own private channel if allowed:
                if($this->isAllowedToCreatePrivateChannel()) {
                    array_push($channels, $this->getPrivateChannelID());
                }
                // Add the invitation channels:
                foreach($this->getInvitations() as $channelID) {
                    if(!in_array($channelID, $channels)) {
                        array_push($channels, $channelID);
                    }
                }
                $this->insertChatBotMessage(
                    $this->getPrivateMessageID(),
                    '/who '.implode(' ', $this->getOnlineUsers($channels))
                );
            }
        } else {
            $channelName = $textParts[1];
            $channelID = $this->getChannelIDFromChannelName($channelName);
            if(!$this->validateChannel($channelID)) {
                // Invalid channel:
                $this->insertChatBotMessage(
                    $this->getPrivateMessageID(),
                    '/error InvalidChannelName '.$channelName
                );
            } else {
                // Get online users for the given channel:
                $onlineUsers = $this->getOnlineUsers(array($channelID));
                if(count($onlineUsers) > 0) {
                    $this->insertChatBotMessage(
                        $this->getPrivateMessageID(),
                        '/whoChannel '.$channelName.' '.implode(' ', $onlineUsers)
                    );
                } else {
                    $this->insertChatBotMessage(
                        $this->getPrivateMessageID(),
                        '/whoEmpty -'
                    );
                }
            }
        }
    }

    function insertParsedMessageList($textParts) {
        // Get the names of all accessible channels:
        $channelNames = $this->getChannelNames();
        // Add the own private channel, if allowed:
        if($this->isAllowedToCreatePrivateChannel()) {
            array_push($channelNames, $this->getPrivateChannelName());
        }
        // Add the invitation channels:
        foreach($this->getInvitations() as $channelID) {
            $channelName = $this->getChannelNameFromChannelID($channelID);
            if($channelName !== null && !in_array($channelName, $channelNames)) {
                array_push($channelNames, $channelName);
            }
        }
        $this->insertChatBotMessage(
            $this->getPrivateMessageID(),
            '/list '.implode(' ', $channelNames)
        );
    }

    function insertParsedMessageWhereis($textParts) {
        if(count($textParts) == 1) {
            $this->insertChatBotMessage(
                $this->getPrivateMessageID(),
                '/error MissingUserName'
            );
        } else {
            // Get UserID from UserName:
            $whereisUserID = $this->getIDFromName($textParts[1]);
            if($whereisUserID === null) {
                $this->insertChatBotMessage(
                    $this->getPrivateMessageID(),
                    '/error UserNameNotFound '.$textParts[1]
                );
            } else {
                $channelID = $this->getChannelFromID($whereisUserID);
                if($this->validateChannel($channelID)) {
                    $channelName = $this->getChannelNameFromChannelID($channelID);
                } else {
                    $channelName = null;
                }
                if($channelName === null) {
                    $this->insertChatBotMessage(
                        $this->getPrivateMessageID(),
                        '/error UserNameNotFound '.$textParts[1]
                    );
                } else {
                    // List user information:
                    $this->insertChatBotMessage(
                        $this->getPrivateMessageID(),
                        '/whereis '.$textParts[1].' '.$channelName
                    );
                }
            }
        }
    }

    function insertParsedMessageWhois($textParts) {
        // Only moderators/admins:
        if($this->getUserRole() == AJAX_CHAT_ADMIN || $this->getUserRole() == AJAX_CHAT_MODERATOR || $this->getUserRole() == PURPLE || $this->getUserRole() == CMOD) {
//      if($this->getUserRole() == AJAX_CHAT_ADMIN) {
            if(count($textParts) == 1) {
                $this->insertChatBotMessage(
                    $this->getPrivateMessageID(),
                    '/error MissingUserName'
                );
            } else {
                // Get UserID from UserName:
                $whoisUserID = $this->getIDFromName($textParts[1]);
                if($whoisUserID === null) {
                    $this->insertChatBotMessage(
                        $this->getPrivateMessageID(),
                        '/error UserNameNotFound '.$textParts[1]
                    );
                } else {
                    // List user information:
                    $this->insertChatBotMessage(
                        $this->getPrivateMessageID(),
                        '/whois '.$textParts[1].' '.$this->getIPFromID($whoisUserID)
                    );
                }
            }
        } else {
            $this->insertChatBotMessage(
                $this->getPrivateMessageID(),
                '/error CommandNotAllowed '.$textParts[0]
            );
        }
    }

    function insertParsedMessageRoll($textParts) {
            if(count($textParts) == 1) {
                // default is one d6:
                $text = '/roll '.$this->getUserName().' 1d6 '.$this->rollDice(6);
            } else {
                $diceParts = explode('d', $textParts[1]);
                if(count($diceParts) == 2) {
                    $number = (int)$diceParts[0];
                    $sides = (int)$diceParts[1];

                    // Dice number must be an integer between 1 and 100, else roll only one:
                    $number = ($number > -1 && $number < 100) ?  $number : 1;

                    // Sides must be an integer between 1 and 100, else take 6:
                    $sides = ($sides > -1 && $sides < 100) ?  $sides : 6;

                    $text = '/roll '.$this->getUserName().' '.$number.'d'.$sides.' ';
                    for($i=0; $i<$number; $i++) {
                        if($i != 0)
                            $text .= ',';
                        switch($this->getUserID()) { // snowflake
                            case 3://303:
                                $text .= str_shuffle('bird');
                                break;

                            case 0://230:
                                $text .= str_shuffle('portugal');
                                break;

                            case 15://21:
                                $text .= str_shuffle('divorce');
                                break;

                            case 2://8:
                                $text .= str_shuffle('mewow');
                                break;

                            default:
                                $text .= $this->rollDice($sides);
                        }
                    }
                } else {
                    // if dice syntax is invalid, roll one d6:
                    $text = '/roll '.$this->getUserName().' 1d6 '.$this->rollDice(6);
                }
            }
            $this->insertChatBotMessage(
                $this->getChannel(),
                $text
            );
    }

    function insertParsedMessageNick($textParts) {
        if($this->getUserRole() == AJAX_CHAT_ADMIN || $this->getUserRole() == AJAX_CHAT_MODERATOR || $this->getUserRole() == PURPLE || $this->getUserRole() == CMOD || $this->getUserRole() == BOTS || $this->getUserRole() == DONATOR) {
            if(!$this->getConfig('allowNickChange') ||
                (!$this->getConfig('allowGuestUserName') && $this->getUserRole() == AJAX_CHAT_GUEST)) {
                $this->insertChatBotMessage(
                    $this->getPrivateMessageID(),
                    '/error CommandNotAllowed '.$textParts[0]
                );
            } elseif(count($textParts) == 1) {
                $oldUserName = $this->getUserName();
                $newUserName = $this->getLoginUserName();
                if($oldUserName != $newUserName) {
                    $this->setUserName($newUserName);
                    $this->updateOnlineList();
                    // Add info message to update the client-side stored userName:
                    $this->addInfoMessage($this->getUserName(), 'userName');
                    $this->insertChatBotMessage(
                        $this->getChannel(),
                        '/nick '.$oldUserName.' '.$newUserName,
                        null,
                        2
                    );
                } else {
                    $this->insertChatBotMessage(
                        $this->getPrivateMessageID(),
                        '/error MissingUserName'
                    );
                }
            } else {
                $newUserName = implode(' ', array_slice($textParts, 1));
                if($newUserName == $this->getLoginUserName()) {
                    // Allow the user to regain the original login userName:
                    $prefix = '';
                    $suffix = '';
                } else if($this->getUserRole() == AJAX_CHAT_GUEST) {
                    $prefix = $this->getConfig('guestUserPrefix');
                    $suffix = $this->getConfig('guestUserSuffix');
                } else {
                    $prefix = $this->getConfig('changedNickPrefix');
                    $suffix = $this->getConfig('changedNickSuffix');
                }
                $maxLength =    $this->getConfig('userNameMaxLength')
                                - $this->stringLength($prefix)
                                - $this->stringLength($suffix);
                $newUserName = $this->trimString($newUserName, 'UTF-8', $maxLength, true);
                if(!$newUserName) {
                    $this->insertChatBotMessage(
                        $this->getPrivateMessageID(),
                        '/error InvalidUserName'
                    );
                } else {
                    $newUserName = $prefix.$newUserName.$suffix;
                    if($this->isUserNameInUse($newUserName)) {
                        $this->insertChatBotMessage(
                            $this->getPrivateMessageID(),
                            '/error UserNameInUse'
                        );
                    } else {
                        $oldUserName = $this->getUserName();
                        $this->setUserName($newUserName);
                        $this->updateOnlineList();
                        // Add info message to update the client-side stored userName:
                        $this->addInfoMessage($this->getUserName(), 'userName');
                        $this->insertChatBotMessage(
                            $this->getChannel(),
                            '/nick '.$oldUserName.' '.$newUserName,
                            null,
                            2
                        );
                    }
                }
            }
        } else {
            $this->insertChatBotMessage(
                $this->getPrivateMessageID(),
                '/error CommandNotAllowed '.$textParts[0]
            );
        }
    }

    function insertParsedMessageChangeNick($textParts) {
        if($this->getUserRole() == AJAX_CHAT_ADMIN || $this->getUserRole() == AJAX_CHAT_MODERATOR) {
            if(count($textParts) == 1) {
                $this->insertChatBotMessage(
                    $this->getPrivateMessageID(),
                    '/error MissingUserName'
                );
            } else {
                $newUserName = implode(' ', array_slice($textParts, 1));
                $oldUserName = $this->getUserName();
                $this->setUserName($newUserName);
                $this->updateOnlineList();
                $this->addInfoMessage($this->getUserName(), 'userName');
            }
        }
    }

    function insertMessage($text) {
        if(!$this->isAllowedToWriteMessage())
            return;

        if(!$this->floodControl())
            return;

        $text = $this->trimMessageText($text);
        if($text == '')
            return;

        if(!$this->onNewMessage($text))
            return;

        $text = $this->replaceCustomText($text);

        $this->insertParsedMessage($text);
    }

    function deleteMessage($messageID) {
        // Retrieve the channel of the given message:
        $sql = 'SELECT
                    channel
                FROM
                    '.$this->getDataBaseTable('messages').'
                WHERE
                    id='.$this->db->makeSafe($messageID).';';

        // Create a new SQL query:
        $result = $this->db->sqlQuery($sql);

        // Stop if an error occurs:
        if($result->error()) {
            echo $result->getError();
            die();
        }

        $row = $result->fetch();

        if($row['channel'] !== null) {
            $channel = $row['channel'];

            if($this->getUserRole() == AJAX_CHAT_ADMIN) {
                $condition = '';
            } else if($this->getUserRole() == CMOD || $this->getUserRole() == PURPLE) {
                $condition = '  AND
                                    NOT (userRole='.$this->db->makeSafe(AJAX_CHAT_MODERATOR).')
                                AND
                                    NOT (userRole='.$this->db->makeSafe(AJAX_CHAT_ADMIN).')
                                AND
                                    NOT (userRole='.$this->db->makeSafe(AJAX_CHAT_CHATBOT).')';
            } else if($this->getUserRole() == AJAX_CHAT_MODERATOR) {
                $condition = '  AND
                                    NOT (userRole='.$this->db->makeSafe(AJAX_CHAT_ADMIN).')
                                AND
                                    NOT (userRole='.$this->db->makeSafe(AJAX_CHAT_CHATBOT).')';
            } else if($this->getUserRole() == AJAX_CHAT_USER && $this->getConfig('allowUserMessageDelete')) {
                $condition = 'AND
                                (
                                userID='.$this->db->makeSafe($this->getUserID()).'
                                OR
                                    (
                                    channel = '.$this->db->makeSafe($this->getPrivateMessageID()).'
                                    OR
                                    channel = '.$this->db->makeSafe($this->getPrivateChannelID()).'
                                    )
                                    AND
                                        NOT (userRole='.$this->db->makeSafe(AJAX_CHAT_ADMIN).')
                                    AND
                                        NOT (userRole='.$this->db->makeSafe(AJAX_CHAT_CHATBOT).')
                                )';
            } else {
                return false;
            }

            // Remove given message from the database:
            $sql = 'DELETE FROM
                        '.$this->getDataBaseTable('messages').'
                    WHERE
                        id='.$this->db->makeSafe($messageID).'
                        '.$condition.';';

            // Create a new SQL query:
            $result = $this->db->sqlQuery($sql);

            // Stop if an error occurs:
            if($result->error()) {
                echo $result->getError();
                die();
            }

            if($result->affectedRows() == 1) {
                // Insert a deletion command to remove the message from the clients chatlists:
                $this->insertChatBotMessage($channel, '/delete '.$messageID);
                return true;
            }
        }
        return false;
    }

    function floodControl() {
        // Admins can do whatever the fuck they want:
        if($this->getUserRole() == AJAX_CHAT_ADMIN || $this->getUserRole() == AJAX_CHAT_MODERATOR || $this->getUserID() == 11) {
            return true;
        }
        $time = time();
        // Check the time of the last inserted message:
        if($this->getInsertedMessagesRateTimeStamp()+60 < $time) {
            $this->setInsertedMessagesRateTimeStamp($time);
            $this->setInsertedMessagesRate(1);
        } else {
            // Increase the inserted messages rate:
            $rate = $this->getInsertedMessagesRate()+1;
            $this->setInsertedMessagesRate($rate);
            // Check if message rate is too high:
            if($rate > $this->getConfig('maxMessageRate')) {
                $this->insertChatBotMessage(
                    $this->getPrivateMessageID(),
                    '/error MaxMessageRate'
                );
                $this->kickUser($this->getUserName(), 10, $this->getUserID());
                $this->insertChatBotMessage($this->getChannel(), "[i][b]".$this->getUsername()."[/b] exceeded the message limit and will now be kicked for 10 minutes.[/i]");
                $this->insertChatBotMessage($this->getChannel(), "/kick ". $this->getUsername());
                // Return false so the message is not inserted:
                return false;
            }
        }
        return true;
    }

    function isAllowedToWriteMessage() {
        if($this->getUserRole() != AJAX_CHAT_GUEST)
            return true;
        if($this->getConfig('allowGuestWrite'))
            return true;
        return false;
    }

    function insertChatBotMessage($channelID, $messageText, $ip=null, $mode=0) {
        $this->insertCustomMessage(
            $this->getConfig('chatBotID'),
            $this->getConfig('chatBotName'),
            AJAX_CHAT_CHATBOT,
            $channelID,
            $messageText,
            $ip,
            $mode
        );
    }

    function insertCustomMessage($userID, $userName, $userRole, $channelID, $text, $ip=null, $mode=0) {
        // The $mode parameter is used for socket updates:
        // 0 = normal messages
        // 1 = channel messages (e.g. login/logout, channel enter/leave, kick)
        // 2 = messages with online user updates (nick)

        $ip = $ip ? $ip : $_SERVER['REMOTE_ADDR'];

        $sql = 'INSERT INTO '.$this->getDataBaseTable('messages').'(
                                userID,
                                userName,
                                userRole,
                                channel,
                                dateTime,
                                ip,
                                text
                            )
                VALUES (
                    '.$this->db->makeSafe($userID).',
                    '.$this->db->makeSafe($userName).',
                    '.$this->db->makeSafe($userRole).',
                    '.$this->db->makeSafe($channelID).',
                    NOW(),
                    '.$this->db->makeSafe($this->ipToStorageFormat($ip)).',
                    '.$this->db->makeSafe($text).'
                );';

        // Create a new SQL query:
        $result = $this->db->sqlQuery($sql);

        // Stop if an error occurs:
        if($result->error()) {
            echo $result->getError();
            die();
        }

        if($this->getConfig('socketServerEnabled')) {
            $this->sendSocketMessage(
                $this->getSocketBroadcastMessage(
                    $this->db->getLastInsertedID(),
                    time(),
                    $userID,
                    $userName,
                    $userRole,
                    $channelID,
                    $text,
                    $mode
                )
            );
        }
    }

    function getSocketBroadcastMessage(
        $messageID,
        $timeStamp,
        $userID,
        $userName,
        $userRole,
        $channelID,
        $text,
        $mode
        ) {
        // The $mode parameter:
        // 0 = normal messages
        // 1 = channel messages (e.g. login/logout, channel enter/leave, kick)
        // 2 = messages with online user updates (nick)

        // Get the message XML content:
        $xml = '<root chatID="'.$this->getConfig('socketServerChatID').'" channelID="'.$channelID.'" mode="'.$mode.'">';
        if($mode) {
            // Add the list of online users if the user list has been updated ($mode > 0):
            $xml .= $this->getChatViewOnlineUsersXML(array($channelID));
        }
        if($mode != 1 || $this->getConfig('showChannelMessages')) {
            $xml .= '<messages>';
            $xml .= $this->getChatViewMessageXML(
                $messageID,
                $timeStamp,
                $userID,
                $userName,
                $userRole,
                $channelID,
                $text
            );
            $xml .= '</messages>';
        }
        $xml .= '</root>';
        return $xml;
    }

    function sendSocketMessage($message) {
        // Open a TCP socket connection to the socket server:
        if($socket = @socket_create(AF_INET, SOCK_STREAM, SOL_TCP)) {
            if(@socket_connect($socket, $this->getConfig('socketServerIP'), $this->getConfig('socketServerPort'))) {
                // Append a null-byte to the string as EOL (End Of Line) character
                // which is required by Flash XML socket communication:
                $message .= "\0";
                @socket_write(
                    $socket,
                    $message,
                    strlen($message) // Using strlen to count the bytes instead of the number of UTF-8 characters
                );
            }
            @socket_close($socket);
        }
    }

    function updateSocketAuthentication($userID, $socketRegistrationID=null, $channels=null) {
        // If no $socketRegistrationID or no $channels are given the authentication is removed for the given user:
        $authentication = '<authenticate chatID="'.$this->getConfig('socketServerChatID').'" userID="'.$userID.'" regID="'.$socketRegistrationID.'">';
        if($channels) {
            foreach($channels as $channelID) {
                $authentication .= '<channel id="'.$channelID.'"/>';
            }
        }
        $authentication .= '</authenticate>';
        $this->sendSocketMessage($authentication);
    }

    function setSocketRegistrationID($value) {
        $this->setSessionVar('SocketRegistrationID', $value);
    }

    function getSocketRegistrationID() {
        return $this->getSessionVar('SocketRegistrationID');
    }

    function rollDice($sides) {
        // seed with microseconds since last "whole" second:
        //mt_srand((double)microtime()*1000000);

        return mt_rand(1, $sides);
    }

    function kickUser($userName, $banMinutes=null, $userID=null) {
        if($userID === null) {
            $userID = $this->getIDFromName($userName);
        }
        if($userID === null) {
            return;
        }

        $banMinutes = ($banMinutes !== null) ? $banMinutes : $this->getConfig('defaultBanTime');

        if($banMinutes) {
            // Ban User for the given time in minutes:
            $this->banUser($userName, $banMinutes, $userID);
        }

        // Remove given User from online list:
        $sql = 'DELETE FROM
                    '.$this->getDataBaseTable('online').'
                WHERE
                    userID = '.$this->db->makeSafe($userID).';';

        // Create a new SQL query:
        $result = $this->db->sqlQuery($sql);

        // Stop if an error occurs:
        if($result->error()) {
            echo $result->getError();
            die();
        }

        // Update the socket server authentication for the kicked user:
        if($this->getConfig('socketServerEnabled')) {
            $this->updateSocketAuthentication($userID);
        }

        $this->removeUserFromOnlineUsersData($userID);
    }

    function getBannedUsersData($key=null, $value=null) {
        if($this->_bannedUsersData === null) {
            $this->_bannedUsersData = array();

            $sql = 'SELECT
                        userID,
                        userName,
                        ip
                    FROM
                        '.$this->getDataBaseTable('bans').'
                    WHERE
                        NOW() < dateTime;';

            // Create a new SQL query:
            $result = $this->db->sqlQuery($sql);

            // Stop if an error occurs:
            if($result->error()) {
                echo $result->getError();
                die();
            }

            while($row = $result->fetch()) {
                $row['ip'] = $this->ipFromStorageFormat($row['ip']);
                array_push($this->_bannedUsersData, $row);
            }

            $result->free();
        }

        if($key) {
            $bannedUsersData = array();
            foreach($this->_bannedUsersData as $bannedUserData) {
                if(!isset($bannedUserData[$key])) {
                    return $bannedUsersData;
                }
                if($value) {
                    if($bannedUserData[$key] == $value) {
                        array_push($bannedUsersData, $bannedUserData);
                    } else {
                        continue;
                    }
                } else {
                    array_push($bannedUsersData, $bannedUserData[$key]);
                }
            }
            return $bannedUsersData;
        }

        return $this->_bannedUsersData;
    }

    function getBannedUsers() {
        return $this->getBannedUsersData('userName');
    }

    function banUser($userName, $banMinutes=null, $userID=null) {
        if($userID === null) {
            $userID = $this->getIDFromName($userName);
        }
        $ip = $this->getIPFromID($userID);
        if(!$ip || $userID === null) {
            return;
        }

        // Remove expired bans:
        $this->removeExpiredBans();

        $banMinutes = (int)$banMinutes;
        if(!$banMinutes) {
            // If banMinutes is not a valid integer, use the defaultBanTime:
            $banMinutes = $this->getConfig('defaultBanTime');
        }

        $sql = 'INSERT INTO '.$this->getDataBaseTable('bans').'(
                    userID,
                    userName,
                    dateTime,
                    ip
                )
                VALUES (
                    '.$this->db->makeSafe($userID).',
                    '.$this->db->makeSafe($userName).',
                    DATE_ADD(NOW(), interval '.$this->db->makeSafe($banMinutes).' MINUTE),
                    '.$this->db->makeSafe($this->ipToStorageFormat($ip)).'
                );';

        // Create a new SQL query:
        $result = $this->db->sqlQuery($sql);

        // Stop if an error occurs:
        if($result->error()) {
            echo $result->getError();
            die();
        }
    }

    function unbanUser($userName) {
        $sql = 'DELETE FROM
                    '.$this->getDataBaseTable('bans').'
                WHERE
                    userName = '.$this->db->makeSafe($userName).';';

        // Create a new SQL query:
        $result = $this->db->sqlQuery($sql);

        // Stop if an error occurs:
        if($result->error()) {
            echo $result->getError();
            die();
        }
    }

    function removeExpiredBans() {
        $sql = 'DELETE FROM
                    '.$this->getDataBaseTable('bans').'
                WHERE
                    dateTime < NOW();';

        // Create a new SQL query:
        $result = $this->db->sqlQuery($sql);

        // Stop if an error occurs:
        if($result->error()) {
            echo $result->getError();
            die();
        }
    }

    function setInactive($userID, $userName=null) {
        $condition = 'userID='.$this->db->makeSafe($userID);
        if($userName !== null) {
            $condition .= ' OR userName='.$this->db->makeSafe($userName);
        }
        $sql = 'UPDATE
                    '.$this->getDataBaseTable('online').'
                SET
                    dateTime = DATE_SUB(NOW(), interval '.(intval($this->getConfig('inactiveTimeout'))+1).' MINUTE)
                WHERE
                    '.$condition.';';

        // Create a new SQL query:
        $result = $this->db->sqlQuery($sql);

        // Stop if an error occurs:
        if($result->error()) {
            echo $result->getError();
            die();
        }

        $this->resetOnlineUsersData();
    }

    function removeInactive() {
        $sql = 'SELECT
                    userID,
                    userName,
                    channel
                FROM
                    '.$this->getDataBaseTable('online').'
                WHERE
                    NOW() > DATE_ADD(dateTime, interval '.$this->getConfig('inactiveTimeout').' MINUTE);';

        // Create a new SQL query:
        $result = $this->db->sqlQuery($sql);

        // Stop if an error occurs:
        if($result->error()) {
            echo $result->getError();
            die();
        }

        if($result->numRows() > 0) {
            $condition = '';
            while($row = $result->fetch()) {
                if(!empty($condition))
                    $condition .= ' OR ';
                // Add userID to condition for removal:
                $condition .= 'userID='.$this->db->makeSafe($row['userID']);

                // Update the socket server authentication for the kicked user:
                if($this->getConfig('socketServerEnabled')) {
                    $this->updateSocketAuthentication($row['userID']);
                }

                $this->removeUserFromOnlineUsersData($row['userID']);

                // Insert logout timeout message:
                $text = '/logout '.$row['userName'].' Timeout';
                $this->insertChatBotMessage(
                    $row['channel'],
                    $text,
                    null,
                    1
                );
            }

            $result->free();

            $sql = 'DELETE FROM
                        '.$this->getDataBaseTable('online').'
                    WHERE
                        '.$condition.';';

            // Create a new SQL query:
            $result = $this->db->sqlQuery($sql);

            // Stop if an error occurs:
            if($result->error()) {
                echo $result->getError();
                die();
            }
        }
    }

    function updateOnlineStatus() {
        // Update online status every 50 seconds (this allows update requests to be in time):
        if(!$this->getStatusUpdateTimeStamp() || ((time() - $this->getStatusUpdateTimeStamp()) > 50)) {
            $this->updateOnlineList();
            $this->setStatusUpdateTimeStamp(time());
        }
    }

    function checkAndRemoveInactive() {
        // Remove inactive users every inactiveCheckInterval:
        if(!$this->getInactiveCheckTimeStamp() || ((time() - $this->getInactiveCheckTimeStamp()) > $this->getConfig('inactiveCheckInterval')*60)) {
            $this->removeInactive();
            $this->setInactiveCheckTimeStamp(time());
        }
    }

    function sendXMLMessages() {
        $httpHeader = new AJAXChatHTTPHeader('UTF-8', 'text/xml');

        // Send HTTP header:
        $httpHeader->send();

        // Output XML messages:
        echo $this->getXMLMessages();
    }

    function getXMLMessages() {
        switch($this->getView()) {
            case 'chat':
                return $this->getChatViewXMLMessages();
            case 'teaser':
                return $this->getTeaserViewXMLMessages();
            case 'logs':
                return $this->getLogsViewXMLMessages();
            default:
                return $this->getLogoutXMLMessage();
        }
    }

    function getMessageCondition() {
        $condition =    'id > '.$this->db->makeSafe($this->getRequestVar('lastID')).'
                        AND (
                            channel = '.$this->db->makeSafe($this->getChannel()).'
                            OR
                            channel = '.$this->db->makeSafe($this->getPrivateMessageID()).'
                        )
                        AND
                        ';
        if($this->getConfig('requestMessagesPriorChannelEnter') ||
            ($this->getConfig('requestMessagesPriorChannelEnterList') && in_array($this->getChannel(), $this->getConfig('requestMessagesPriorChannelEnterList')))) {
            $condition .= 'NOW() < DATE_ADD(dateTime, interval '.$this->getConfig('requestMessagesTimeDiff').' HOUR)';
        } else {
            $condition .= 'dateTime >= \''.date('Y-m-d H:i:s', $this->getChannelEnterTimeStamp()).'\'';
        }
        return $condition;
    }

    function getMessageFilter() {
            $filterChannelMessages = '';
            if(!$this->getConfig('showChannelMessages') || $this->getRequestVar('shoutbox')) {
                $filterChannelMessages = '  AND NOT (
                                            text LIKE (\'/login%\')
                                            OR
                                            text LIKE (\'/logout%\')
                                            OR
                                            text LIKE (\'/channelEnter%\')
                                            OR
                                            text LIKE (\'/channelLeave%\')
                                            OR
                                            text LIKE (\'/kick%\')
                                        )';
            }
            return $filterChannelMessages;
    }

    function getInfoMessagesXML() {
        $xml = '<infos>';
        // Go through the info messages:
        foreach($this->getInfoMessages() as $type=>$infoArray) {
            foreach($infoArray as $info) {
                $xml .= '<info type="'.$type.'">';
                $xml .= '<![CDATA['.$this->encodeSpecialChars($info).']]>';
                $xml .= '</info>';
            }
        }
        $xml .= '</infos>';
        return $xml;
    }

    function getChatViewOnlineUsersXML($channelIDs) {
        // Get the online users for the given channels:
        $onlineUsersData = $this->getOnlineUsersData($channelIDs);
        $xml = '<users>';
        foreach($onlineUsersData as $onlineUserData) {
            $xml .= '<user';
            $xml .= ' userID="'.$onlineUserData['userID'].'"';
            $xml .= ' userRole="'.$onlineUserData['userRole'].'"';
            $xml .= ' channelID="'.$onlineUserData['channel'].'"';
            $xml .= '>';
            $xml .= '<![CDATA['.$this->encodeSpecialChars($onlineUserData['userName']).']]>';
            $xml .= '</user>';
        }
        $xml .= '</users>';
        return $xml;
    }

    function getLogoutXMLMessage() {
        $xml = '<?xml version="1.0" encoding="UTF-8"?>';
        $xml .= '<root>';
        $xml .= '<infos>';
        $xml .= '<info type="logout">';
        $xml .= '<![CDATA['.$this->encodeSpecialChars($this->getConfig('logoutData')).']]>';
        $xml .= '</info>';
        $xml .= '</infos>';
        $xml .= '</root>';
        return $xml;
    }

    function getChatViewMessageXML(
        $messageID,
        $timeStamp,
        $userID,
        $userName,
        $userRole,
        $channelID,
        $text
        ) {
        $message = '<message';
        $message .= ' id="'.$messageID.'"';
        $message .= ' dateTime="'.date('r', $timeStamp).'"';
        $message .= ' userID="'.$userID.'"';
        $message .= ' userRole="'.$userRole.'"';
        $message .= ' channelID="'.$channelID.'"';
        $message .= '>';
        $message .= '<username><![CDATA['.$this->encodeSpecialChars($userName).']]></username>';
        $message .= '<text><![CDATA['.$this->encodeSpecialChars($text).']]></text>';
        $message .= '</message>';
        return $message;
    }

    function getChatViewMessagesXML() {
        // Get the last messages in descending order (this optimises the LIMIT usage):
        $sql = 'SELECT
                    id,
                    userID,
                    userName,
                    userRole,
                    channel AS channelID,
                    UNIX_TIMESTAMP(dateTime) AS timeStamp,
                    text
                FROM
                    '.$this->getDataBaseTable('messages').'
                WHERE
                    '.$this->getMessageCondition().'
                    '.$this->getMessageFilter().'
                ORDER BY
                    id
                    DESC
                LIMIT '.$this->getConfig('requestMessagesLimit').';';

        // Create a new SQL query:
        $result = $this->db->sqlQuery($sql);

        // Stop if an error occurs:
        if($result->error()) {
            echo $result->getError();
            die();
        }

        $messages = '';

        // Add the messages in reverse order so it is ascending again:
        while($row = $result->fetch()) {
            $message = $this->getChatViewMessageXML(
                $row['id'],
                $row['timeStamp'],
                $row['userID'],
                $row['userName'],
                $row['userRole'],
                $row['channelID'],
                $row['text']
            );
            $messages = $message.$messages;
        }
        $result->free();

        $messages = '<messages>'.$messages.'</messages>';
        return $messages;
    }

    function getChatViewXMLMessages() {
        $xml = '<?xml version="1.0" encoding="UTF-8"?>';
        $xml .= '<root>';
        $xml .= $this->getInfoMessagesXML();
        $xml .= $this->getChatViewOnlineUsersXML(array($this->getChannel()));
        $xml .= $this->getChatViewMessagesXML();
        $xml .= '</root>';
        return $xml;
    }

    function getTeaserMessageCondition() {
        $channelID = $this->getValidRequestChannelID();
        $condition =    'channel = '.$this->db->makeSafe($channelID).'
                        AND
                        ';
        if($this->getConfig('requestMessagesPriorChannelEnter') ||
            ($this->getConfig('requestMessagesPriorChannelEnterList') && in_array($channelID, $this->getConfig('requestMessagesPriorChannelEnterList')))) {
            $condition .= 'NOW() < DATE_ADD(dateTime, interval '.$this->getConfig('requestMessagesTimeDiff').' HOUR)';
        } else {
            // Teaser content may not be shown for this channel:
            $condition .= '0 = 1';
        }
        return $condition;
    }

    function getTeaserViewMessagesXML() {
        // Get the last messages in descending order (this optimises the LIMIT usage):
        $sql = 'SELECT
                    id,
                    userID,
                    userName,
                    userRole,
                    channel AS channelID,
                    UNIX_TIMESTAMP(dateTime) AS timeStamp,
                    text
                FROM
                    '.$this->getDataBaseTable('messages').'
                WHERE
                    '.$this->getTeaserMessageCondition().'
                    '.$this->getMessageFilter().'
                ORDER BY
                    id
                    DESC
                LIMIT '.$this->getConfig('requestMessagesLimit').';';

        // Create a new SQL query:
        $result = $this->db->sqlQuery($sql);

        // Stop if an error occurs:
        if($result->error()) {
            echo $result->getError();
            die();
        }

        $messages = '';

        // Add the messages in reverse order so it is ascending again:
        while($row = $result->fetch()) {
            $message = '';
            $message .= '<message';
            $message .= ' id="'.$row['id'].'"';
            $message .= ' dateTime="'.date('r', $row['timeStamp']).'"';
            $message .= ' userID="'.$row['userID'].'"';
            $message .= ' userRole="'.$row['userRole'].'"';
            $message .= ' channelID="'.$row['channelID'].'"';
            $message .= '>';
            $message .= '<username><![CDATA['.$this->encodeSpecialChars($row['userName']).']]></username>';
            $message .= '<text><![CDATA['.$this->encodeSpecialChars($row['text']).']]></text>';
            $message .= '</message>';
            $messages = $message.$messages;
        }
        $result->free();

        $messages = '<messages>'.$messages.'</messages>';
        return $messages;
    }

    function getTeaserViewXMLMessages() {
        $xml = '<?xml version="1.0" encoding="UTF-8"?>';
        $xml .= '<root>';
        $xml .= $this->getInfoMessagesXML();
        $xml .= $this->getTeaserViewMessagesXML();
        $xml .= '</root>';
        return $xml;
    }

    function getLogsViewCondition() {
        $condition = 'id > '.$this->db->makeSafe($this->getRequestVar('lastID'));

        // Check the channel condition:
        switch($this->getRequestVar('channelID')) {
            case '-3':
                // Just display messages from all accessible channels
                if($this->getUserRole() != AJAX_CHAT_ADMIN) {
                    $condition .= ' AND (channel = '.$this->db->makeSafe($this->getPrivateMessageID());
                    $condition .= ' OR channel = '.$this->db->makeSafe($this->getPrivateChannelID());
                    foreach($this->getChannels() as $channel) {
                        if($this->getConfig('logsUserAccessChannelList') && !in_array($channel, $this->getConfig('logsUserAccessChannelList'))) {
                            continue;
                        }
                        $condition .= ' OR channel = '.$this->db->makeSafe($channel);
                    }
                    $condition .= ')';
                }
                break;
            case '-2':
                if($this->getUserRole() != AJAX_CHAT_ADMIN) {
                    $condition .= ' AND channel = '.($this->getPrivateMessageID());
                } else {
                    $condition .= ' AND channel > '.($this->getConfig('privateMessageDiff')-1);
                }
                break;
            case '-1':
                if($this->getUserRole() != AJAX_CHAT_ADMIN) {
                    $condition .= ' AND channel = '.($this->getPrivateChannelID());
                } else {
                    $condition .= ' AND (channel > '.($this->getConfig('privateChannelDiff')-1).' AND channel < '.($this->getConfig('privateMessageDiff')).')';
                }
                break;
            default:
                if(($this->getUserRole() == AJAX_CHAT_ADMIN || !$this->getConfig('logsUserAccessChannelList') || in_array($this->getRequestVar('channelID'), $this->getConfig('logsUserAccessChannelList')))
                    && $this->validateChannel($this->getRequestVar('channelID'))) {
                    $condition .= ' AND channel = '.$this->db->makeSafe($this->getRequestVar('channelID'));
                } else {
                    // No valid channel:
                    $condition .= ' AND 0 = 1';
                }
        }

        // Check the period condition:
        $hour   = ($this->getRequestVar('hour') === null || $this->getRequestVar('hour') > 23 || $this->getRequestVar('hour') < 0) ? null : $this->getRequestVar('hour');
        $day    = ($this->getRequestVar('day') === null || $this->getRequestVar('day') > 31 || $this->getRequestVar('day') < 1) ? null : $this->getRequestVar('day');
        $month  = ($this->getRequestVar('month') === null || $this->getRequestVar('month') > 12 || $this->getRequestVar('month') < 1) ? null : $this->getRequestVar('month');
        $year   = ($this->getRequestVar('year') === null || $this->getRequestVar('year') > date('Y') || $this->getRequestVar('year') < $this->getConfig('logsFirstYear')) ? null : $this->getRequestVar('year');

        // If a time (hour) is given but no date (year, month, day), use the current date:
        if($hour !== null) {
            if($day === null)
                $day = date('j');
            if($month === null)
                $month = date('n');
            if($year === null)
                $year = date('Y');
        }

        if($year === null) {
            // No year given, so no period condition
        } else if($month === null) {
            // Define the given year as period:
            $periodStart = mktime(0, 0, 0, 1, 1, $year);
            // The last day in a month can be expressed by using 0 for the day of the next month:
            $periodEnd = mktime(23, 59, 59, 13, 0, $year);
        } else if($day === null) {
            // Define the given month as period:
            $periodStart = mktime(0, 0, 0, $month, 1, $year);
            // The last day in a month can be expressed by using 0 for the day of the next month:
            $periodEnd = mktime(23, 59, 59, $month+1, 0, $year);
        } else if($hour === null){
            // Define the given day as period:
            $periodStart = mktime(0, 0, 0, $month, $day, $year);
            $periodEnd = mktime(23, 59, 59, $month, $day, $year);
        } else {
            // Define the given hour as period:
            $periodStart = mktime($hour, 0, 0, $month, $day, $year);
            $periodEnd = mktime($hour, 59, 59, $month, $day, $year);
        }

        if(isset($periodStart))
            $condition .= ' AND dateTime > \''.date('Y-m-d H:i:s', $periodStart).'\' AND dateTime <= \''.date('Y-m-d H:i:s', $periodEnd).'\'';

        // Check the search condition:
        if($this->getRequestVar('search')) {
            if(($this->getUserRole() == AJAX_CHAT_ADMIN || $this->getUserRole() == AJAX_CHAT_MODERATOR || $this->getUserRole() == PURPLE || $this->getUserRole() == CMOD) && strpos($this->getRequestVar('search'), 'ip=') === 0) {
                // Search for messages with the given IP:
                $ip = substr($this->getRequestVar('search'), 3);
                $condition .= ' AND (ip = '.$this->db->makeSafe($this->ipToStorageFormat($ip)).')';
            } else if(strpos($this->getRequestVar('search'), 'userID=') === 0) {
                // Search for messages with the given userID:
                $userID = substr($this->getRequestVar('search'), 7);
                $condition .= ' AND (userID = '.$this->db->makeSafe($userID).')';
            } else {
                // Use the search value as regular expression on message text and username:
                $condition .= ' AND (userName REGEXP '.$this->db->makeSafe($this->getRequestVar('search')).' OR text REGEXP '.$this->db->makeSafe($this->getRequestVar('search')).')';
            }
        }

        // If no period or search condition is given, just monitor the last messages on the given channel:
        if(!isset($periodStart) && !$this->getRequestVar('search')) {
            $condition .= ' AND NOW() < DATE_ADD(dateTime, interval '.$this->getConfig('logsRequestMessagesTimeDiff').' HOUR)';
        }

        return $condition;
    }

    function getLogsViewMessagesXML() {
        $sql = 'SELECT
                    id,
                    userID,
                    userName,
                    userRole,
                    channel AS channelID,
                    UNIX_TIMESTAMP(dateTime) AS timeStamp,
                    ip,
                    text
                FROM
                    '.$this->getDataBaseTable('messages').'
                WHERE
                    '.$this->getLogsViewCondition().'
                ORDER BY
                    id
                LIMIT '.$this->getConfig('logsRequestMessagesLimit').';';

        // Create a new SQL query:
        $result = $this->db->sqlQuery($sql);

        // Stop if an error occurs:
        if($result->error()) {
            echo $result->getError();
            die();
        }

        $xml = '<messages>';
        while($row = $result->fetch()) {
            $xml .= '<message';
            $xml .= ' id="'.$row['id'].'"';
            $xml .= ' dateTime="'.date('r', $row['timeStamp']).'"';
            $xml .= ' userID="'.$row['userID'].'"';
            $xml .= ' userRole="'.$row['userRole'].'"';
            $xml .= ' channelID="'.$row['channelID'].'"';
            if($this->getUserRole() == AJAX_CHAT_ADMIN || $this->getUserRole() == AJAX_CHAT_MODERATOR || $this->getUserRole() == PURPLE) {
                $xml .= ' ip="'.$this->ipFromStorageFormat($row['ip']).'"';
            }
            $xml .= '>';
            $xml .= '<username><![CDATA['.$this->encodeSpecialChars($row['userName']).']]></username>';
            $xml .= '<text><![CDATA['.$this->encodeSpecialChars($row['text']).']]></text>';
            $xml .= '</message>';
        }
        $result->free();

        $xml .= '</messages>';

        return $xml;
    }

    function getLogsViewXMLMessages() {
        $xml = '<?xml version="1.0" encoding="UTF-8"?>';
        $xml .= '<root>';
        $xml .= $this->getInfoMessagesXML();
        $xml .= $this->getLogsViewMessagesXML();
        $xml .= '</root>';
        return $xml;
    }

    function purgeLogs() {
        $sql = 'DELETE FROM
                    '.$this->getDataBaseTable('messages').'
                WHERE
                    dateTime < DATE_SUB(NOW(), interval '.$this->getConfig('logsPurgeTimeDiff').' DAY);';

        // Create a new SQL query:
        $result = $this->db->sqlQuery($sql);

        // Stop if an error occurs:
        if($result->error()) {
            echo $result->getError();
            die();
        }
    }

    function getInfoMessages($type=null) {
        if(!isset($this->_infoMessages)) {
            $this->_infoMessages = array();
        }
        if($type) {
            if(!isset($this->_infoMessages[$type])) {
                $this->_infoMessages[$type] = array();
            }
            return $this->_infoMessages[$type];
        } else {
            return $this->_infoMessages;
        }
    }

    function addInfoMessage($info, $type='error') {
        if(!isset($this->_infoMessages)) {
            $this->_infoMessages = array();
        }
        if(!isset($this->_infoMessages[$type])) {
            $this->_infoMessages[$type] = array();
        }
        if(!in_array($info, $this->_infoMessages[$type])) {
            array_push($this->_infoMessages[$type], $info);
        }
    }

    function getRequestVars() {
        return $this->_requestVars;
    }

    function getRequestVar($key) {
        if($this->_requestVars && isset($this->_requestVars[$key])) {
            return $this->_requestVars[$key];
        }
        return null;
    }

    function setRequestVar($key, $value) {
        if(!$this->_requestVars) {
            $this->_requestVars = array();
        }
        $this->_requestVars[$key] = $value;
    }

    function getOnlineUsersData($channelIDs=null, $key=null, $value=null) {
        if($this->_onlineUsersData === null) {
            $this->_onlineUsersData = array();

            $sql = 'SELECT
                        userID,
                        userName,
                        userRole,
                        channel,
                        UNIX_TIMESTAMP(dateTime) AS timeStamp,
                        ip
                    FROM
                        '.$this->getDataBaseTable('online').'
                    ORDER BY
                        userName;';

            // Create a new SQL query:
            $result = $this->db->sqlQuery($sql);

            // Stop if an error occurs:
            if($result->error()) {
                echo $result->getError();
                die();
            }

            while($row = $result->fetch()) {
                $row['ip'] = $this->ipFromStorageFormat($row['ip']);
                array_push($this->_onlineUsersData, $row);
            }

            $result->free();
        }

        if($channelIDs || $key) {
            $onlineUsersData = array();
            foreach($this->_onlineUsersData as $userData) {
                if($channelIDs && !in_array($userData['channel'], $channelIDs)) {
                    continue;
                }
                if($key) {
                    if(!isset($userData[$key])) {
                        return $onlineUsersData;
                    }
                    if($value !== null) {
                        if($userData[$key] == $value) {
                            array_push($onlineUsersData, $userData);
                        } else {
                            continue;
                        }
                    } else {
                        array_push($onlineUsersData, $userData[$key]);
                    }
                } else {
                    array_push($onlineUsersData, $userData);
                }
            }
            return $onlineUsersData;
        }

        return $this->_onlineUsersData;
    }

    function removeUserFromOnlineUsersData($userID=null) {
        if(!$this->_onlineUsersData) {
            return;
        }
        $userID = ($userID === null) ? $this->getUserID() : $userID;
        for($i=0; $i<count($this->_onlineUsersData); $i++) {
            if($this->_onlineUsersData[$i]['userID'] == $userID) {
                array_splice($this->_onlineUsersData, $i, 1);
                break;
            }
        }
    }

    function resetOnlineUsersData() {
        $this->_onlineUsersData = null;
    }

    function getOnlineUsers($channelIDs=null) {
        return $this->getOnlineUsersData($channelIDs, 'userName');
    }

    function getOnlineUserIDs($channelIDs=null) {
        return $this->getOnlineUsersData($channelIDs, 'userID');
    }

    function startSession() {
        if(!session_id()) {
            // Set the session name:
            session_name($this->getConfig('sessionName'));

            // Set session cookie parameters:
            session_set_cookie_params(
                0, // The session is destroyed on logout anyway, so no use to set this
                $this->getConfig('sessionCookiePath'),
                $this->getConfig('sessionCookieDomain'),
                $this->getConfig('sessionCookieSecure')
            );

            // Start the session:
            session_start();

            // We started a new session:
            $this->_sessionNew = true;
        }
    }

    function destroySession() {
        if($this->_sessionNew) {
            // Delete all session variables:
            $_SESSION = array();

            // Delete the session cookie:
            if (isset($_COOKIE[session_name()])) {
                setcookie(
                    session_name(),
                    '',
                    time()-42000,
                    $this->getConfig('sessionCookiePath'),
                    $this->getConfig('sessionCookieDomain') ?? '',
                    $this->getConfig('sessionCookieSecure') ?? false
                );
            }

            // Destroy the session:
            session_destroy();
        } else {
            // Unset all session variables starting with the sessionKeyPrefix:
            foreach($_SESSION as $key=>$value) {
                if(strpos($key, $this->getConfig('sessionKeyPrefix')) === 0) {
                    unset($_SESSION[$key]);
                }
            }
        }
    }

    function regenerateSessionID() {
        if($this->_sessionNew) {
            // Regenerate session id:
            @session_regenerate_id(true);
        }
    }

    function getSessionVar($key, $prefix=null) {
        if($prefix === null)
            $prefix = $this->getConfig('sessionKeyPrefix');

        // Return the session value if existing:
        if(isset($_SESSION[$prefix.$key]))
            return $_SESSION[$prefix.$key];
        else
            return null;
    }

    function setSessionVar($key, $value, $prefix=null) {
        if($prefix === null)
            $prefix = $this->getConfig('sessionKeyPrefix');

        // Set the session value:
        $_SESSION[$prefix.$key] = $value;
    }

    function getSessionIP() {
        return $this->getSessionVar('IP');
    }

    function setSessionIP($ip) {
        $this->setSessionVar('IP', $ip);
    }

    function getQueryUserName() {
        return $this->getSessionVar('QueryUserName');
    }

    function setQueryUserName($userName) {
        $this->setSessionVar('QueryUserName', $userName);
    }

    function getInvitations() {
        if($this->_invitations === null) {
            $this->_invitations = array();

            $sql = 'SELECT
                        channel
                    FROM
                        '.$this->getDataBaseTable('invitations').'
                    WHERE
                        userID='.$this->db->makeSafe($this->getUserID()).'
                        AND
                        DATE_SUB(NOW(), interval 1 DAY) < dateTime;';

            // Create a new SQL query:
            $result = $this->db->sqlQuery($sql);

            // Stop if an error occurs:
            if($result->error()) {
                echo $result->getError();
                die();
            }

            while($row = $result->fetch()) {
                array_push($this->_invitations, $row['channel']);
            }

            $result->free();
        }
        return $this->_invitations;
    }

    function removeExpiredInvitations() {
        $sql = 'DELETE FROM
                    '.$this->getDataBaseTable('invitations').'
                WHERE
                    DATE_SUB(NOW(), interval 1 DAY) > dateTime;';

        // Create a new SQL query:
        $result = $this->db->sqlQuery($sql);

        // Stop if an error occurs:
        if($result->error()) {
            echo $result->getError();
            die();
        }
    }

    function addInvitation($userID, $channelID=null) {
        $this->removeExpiredInvitations();

        $channelID = ($channelID === null) ? $this->getChannel() : $channelID;

        $sql = 'INSERT INTO '.$this->getDataBaseTable('invitations').'(
                    userID,
                    channel,
                    dateTime
                )
                VALUES (
                    '.$this->db->makeSafe($userID).',
                    '.$this->db->makeSafe($channelID).',
                    NOW()
                );';

        // Create a new SQL query:
        $result = $this->db->sqlQuery($sql);

        // Stop if an error occurs:
        if($result->error()) {
            echo $result->getError();
            die();
        }
    }

    function removeInvitation($userID, $channelID=null) {
        $channelID = ($channelID === null) ? $this->getChannel() : $channelID;

        $sql = 'DELETE FROM
                    '.$this->getDataBaseTable('invitations').'
                WHERE
                    userID='.$this->db->makeSafe($userID).'
                    AND
                    channel='.$this->db->makeSafe($channelID).';';

        // Create a new SQL query:
        $result = $this->db->sqlQuery($sql);

        // Stop if an error occurs:
        if($result->error()) {
            echo $result->getError();
            die();
        }
    }

    function getUserID() {
        return $this->getSessionVar('UserID');
    }

    function setUserID($id) {
        $this->setSessionVar('UserID', $id);
    }

    function getUserName() {
        return $this->getSessionVar('UserName');
    }

    function setUserName($name) {
        $this->setSessionVar('UserName', $name);
    }

    function getLoginUserName() {
        return $this->getSessionVar('LoginUserName');
    }

    function setLoginUserName($name) {
        $this->setSessionVar('LoginUserName', $name);
    }

    function getUserRole() {
        $userRole = $this->getSessionVar('UserRole');
        if($userRole === null)
            return AJAX_CHAT_GUEST;
        return $userRole;
    }

    function setUserRole($role) {
        $this->setSessionVar('UserRole', $role);
    }

    function getChannel() {
        return $this->getSessionVar('Channel');
    }

    function setChannel($channel) {
        $this->setSessionVar('Channel', $channel);

        // Save the channel enter timestamp:
        $this->setChannelEnterTimeStamp(time());

        // Update the channel authentication for the socket server:
        if($this->getConfig('socketServerEnabled')) {
            $this->updateSocketAuthentication(
                $this->getUserID(),
                $this->getSocketRegistrationID(),
                array($channel,$this->getPrivateMessageID())
            );
        }

        // Reset the logs view socket authentication session var:
        if($this->getSessionVar('logsViewSocketAuthenticated')) {
            $this->setSessionVar('logsViewSocketAuthenticated', false);
        }
    }

    function isLoggedIn() {
        return (bool)$this->getSessionVar('LoggedIn');
    }

    function setLoggedIn($bool) {
        $this->setSessionVar('LoggedIn', $bool);
    }

    function getLoginTimeStamp() {
        return $this->getSessionVar('LoginTimeStamp');
    }

    function setLoginTimeStamp($time) {
        $this->setSessionVar('LoginTimeStamp', $time);
    }

    function getChannelEnterTimeStamp() {
        return $this->getSessionVar('ChannelEnterTimeStamp');
    }

    function setChannelEnterTimeStamp($time) {
        $this->setSessionVar('ChannelEnterTimeStamp', $time);
    }

    function getStatusUpdateTimeStamp() {
        return $this->getSessionVar('StatusUpdateTimeStamp');
    }

    function setStatusUpdateTimeStamp($time) {
        $this->setSessionVar('StatusUpdateTimeStamp', $time);
    }

    function getInactiveCheckTimeStamp() {
        return $this->getSessionVar('InactiveCheckTimeStamp');
    }

    function setInactiveCheckTimeStamp($time) {
        $this->setSessionVar('InactiveCheckTimeStamp', $time);
    }

    function getInsertedMessagesRate() {
        return $this->getSessionVar('InsertedMessagesRate');
    }

    function setInsertedMessagesRate($rate) {
        $this->setSessionVar('InsertedMessagesRate', $rate);
    }

    function getInsertedMessagesRateTimeStamp() {
        return $this->getSessionVar('InsertedMessagesRateTimeStamp');
    }

    function setInsertedMessagesRateTimeStamp($time) {
        $this->setSessionVar('InsertedMessagesRateTimeStamp', $time);
    }

    function getLangCode() {
        // Get the langCode from request or cookie:
        $langCodeCookie = isset($_COOKIE[$this->getConfig('sessionName').'_lang']) ? $_COOKIE[$this->getConfig('sessionName').'_lang'] : null;
        $langCode = $this->getRequestVar('lang') ? $this->getRequestVar('lang') : $langCodeCookie;
        // Check if the langCode is valid:
        if(!in_array($langCode, $this->getConfig('langAvailable'))) {
            // Determine the user language:
            $language = new AJAXChatLanguage($this->getConfig('langAvailable'), $this->getConfig('langDefault'));
            $langCode = $language->getLangCode();
        }
        return $langCode;
    }

    function setLangCodeCookie() {
        setcookie(
            $this->getConfig('sessionName').'_lang',
            $this->getLangCode(),
            time()+60*60*24*$this->getConfig('sessionCookieLifeTime'),
            $this->getConfig('sessionCookiePath'),
            $this->getConfig('sessionCookieDomain') ?? '',
            $this->getConfig('sessionCookieSecure') ?? false
        );
    }

    function removeUnsafeCharacters($str) {
        // Remove NO-WS-CTL, non-whitespace control characters (RFC 2822), decimal 1–8, 11–12, 14–31, and 127:
        return AJAXChatEncoding::removeUnsafeCharacters($str);
    }

    function subString($str, $start=0, $length=null, $encoding='UTF-8') {
        return AJAXChatString::subString($str, $start, $length, $encoding);
    }

    function stringLength($str, $encoding='UTF-8') {
        return AJAXChatString::stringLength($str, $encoding);
    }

    function trimMessageText($text) {
        return $this->trimString($text, 'UTF-8', $this->getConfig('messageTextMaxLength'));
    }

    function trimUserName($userName) {
        return $this->trimString($userName, null, $this->getConfig('userNameMaxLength'), true, true);
    }

    function trimChannelName($channelName) {
        return $this->trimString($channelName, null, null, true, true);
    }

    function trimString($str, $sourceEncoding=null, $maxLength=null, $replaceWhitespace=false, $decodeEntities=false, $htmlEntitiesMap=null) {
        // Make sure the string contains valid unicode:
        $str = $this->convertToUnicode($str, $sourceEncoding);

        // Make sure the string contains no unsafe characters:
        $str = $this->removeUnsafeCharacters($str);

        // Strip whitespace from the beginning and end of the string:
        $str = trim($str);

        if($replaceWhitespace) {
            // Replace any whitespace in the userName with the underscore "_":
            $str = preg_replace('/\s/u', '_', $str);
        }

        if($decodeEntities) {
            // Decode entities:
            $str = $this->decodeEntities($str, 'UTF-8', $htmlEntitiesMap);
        }

        if($maxLength) {
            // Cut the string to the allowed length:
            $str = $this->subString($str, 0, $maxLength);
        }

        return $str;
    }

    function convertToUnicode($str, $sourceEncoding=null) {
        if($sourceEncoding === null) {
            $sourceEncoding = $this->getConfig('sourceEncoding');
        }
        return $this->convertEncoding($str, $sourceEncoding, 'UTF-8');
    }

    function convertFromUnicode($str, $contentEncoding=null) {
        if($contentEncoding === null) {
            $contentEncoding = $this->getConfig('contentEncoding');
        }
        return $this->convertEncoding($str, 'UTF-8', $contentEncoding);
    }

    function convertEncoding($str, $charsetFrom, $charsetTo) {
        return AJAXChatEncoding::convertEncoding($str, $charsetFrom, $charsetTo);
    }

    function encodeEntities($str, $encoding='UTF-8', $convmap=null) {
        return AJAXChatEncoding::encodeEntities($str, $encoding, $convmap);
    }

    function decodeEntities($str, $encoding='UTF-8', $htmlEntitiesMap=null) {
        return AJAXChatEncoding::decodeEntities($str, $encoding, $htmlEntitiesMap);
    }

    function htmlEncode($str) {
        return AJAXChatEncoding::htmlEncode($str, $this->getConfig('contentEncoding'));
    }

    function encodeSpecialChars($str) {
        return AJAXChatEncoding::encodeSpecialChars($str);
    }

    function decodeSpecialChars($str) {
        return AJAXChatEncoding::decodeSpecialChars($str);
    }

    function ipToStorageFormat($ip) {
        if(function_exists('inet_pton')) {
            // ipv4 & ipv6:
            return @inet_pton($ip);
        }
        // Only ipv4:
        return @pack('N',@ip2long($ip));
    }

    function ipFromStorageFormat($ip) {
        if(function_exists('inet_ntop')) {
            // ipv4 & ipv6:
            return @inet_ntop($ip);
        }
        // Only ipv4:
        $unpacked = @unpack('Nlong',$ip);
        if(isset($unpacked['long'])) {
            return @long2ip($unpacked['long']);
        }
        return null;
    }

    function getConfig($key, $subkey=null) {
        if($subkey)
            return $this->_config[$key][$subkey];
        else
            return $this->_config[$key];
    }

    function setConfig($key, $subkey, $value) {
        if($subkey) {
            if(!isset($this->_config[$key])) {
                $this->_config[$key] = array();
            }
            $this->_config[$key][$subkey] = $value;
        } else {
            $this->_config[$key] = $value;
        }
    }

    function getLang($key=null) {
        if(!$this->_lang) {
            // Include the language file:
            $lang = null;
            require AJAX_CHAT_PATH . '/lang/' . $this->getLangCode() . '.php';
            $this->_lang = &$lang;
        }
        if($key === null)
            return $this->_lang;
        if(isset($this->_lang[$key]))
            return str_replace('%flashii%', $this->getConfig('flashiiString'), $this->_lang[$key]);
        return null;
    }

    function getChatURL() {
        if(defined('AJAX_CHAT_URL')) {
            return AJAX_CHAT_URL;
        }

        return
            (isset($_SERVER['HTTPS']) ? 'https://' : 'http://').
            (isset($_SERVER['REMOTE_USER']) ? $_SERVER['REMOTE_USER'].'@' : '').
            (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : ($_SERVER['SERVER_NAME'].
            (isset($_SERVER['HTTPS']) && $_SERVER['SERVER_PORT'] == 443 || $_SERVER['SERVER_PORT'] == 80 ? '' : ':'.$_SERVER['SERVER_PORT']))).
            substr($_SERVER['SCRIPT_NAME'],0, strrpos($_SERVER['SCRIPT_NAME'], '/')+1);
    }

    function getIDFromName($userName) {
        $userDataArray = $this->getOnlineUsersData(null,'userName',$userName);
        if($userDataArray && isset($userDataArray[0])) {
            return $userDataArray[0]['userID'];
        }
        return null;
    }

    function getNameFromID($userID) {
        $userDataArray = $this->getOnlineUsersData(null,'userID',$userID);
        if($userDataArray && isset($userDataArray[0])) {
            return $userDataArray[0]['userName'];
        }
        return null;
    }

    function getChannelFromID($userID) {
        $userDataArray = $this->getOnlineUsersData(null,'userID',$userID);
        if($userDataArray && isset($userDataArray[0])) {
            return $userDataArray[0]['channel'];
        }
        return null;
    }

    function getIPFromID($userID) {
        $userDataArray = $this->getOnlineUsersData(null,'userID',$userID);
        if($userDataArray && isset($userDataArray[0])) {
            return $userDataArray[0]['ip'];
        }
        return null;
    }

    function getRoleFromID($userID) {
        $userDataArray = $this->getOnlineUsersData(null,'userID',$userID);
        if($userDataArray && isset($userDataArray[0])) {
            return $userDataArray[0]['userRole'];
        }
        return null;
    }

    function getChannelNames() {
        return array_flip($this->getChannels());
    }

    function getChannelIDFromChannelName($channelName) {
        if(!$channelName)
            return null;
        $channels = $this->getAllChannels();
        if(array_key_exists($channelName,$channels)) {
            return $channels[$channelName];
        }
        $channelID = null;
        // Check if the requested channel is the own private channel:
        if($channelName == $this->getPrivateChannelName()) {
            return $this->getPrivateChannelID();
        }
        // Try to retrieve a private room ID:
        $strlenChannelName = $this->stringLength($channelName);
        $strlenPrefix = $this->stringLength($this->getConfig('privateChannelPrefix'));
        $strlenSuffix = $this->stringLength($this->getConfig('privateChannelSuffix'));
        if($this->subString($channelName,0,$strlenPrefix) == $this->getConfig('privateChannelPrefix')
            && $this->subString($channelName,$strlenChannelName-$strlenSuffix) == $this->getConfig('privateChannelSuffix')) {
            $userName = $this->subString(
                            $channelName,
                            $strlenPrefix,
                            $strlenChannelName-($strlenPrefix+$strlenSuffix)
                        );
            $userID = $this->getIDFromName($userName);
            if($userID !== null) {
                $channelID = $this->getPrivateChannelID($userID);
            }
        }
        return $channelID;
    }

    function getChannelNameFromChannelID($channelID) {
        foreach($this->getAllChannels() as $key=>$value) {
            if($value == $channelID) {
                return $key;
            }
        }
        // Try to retrieve a private room name:
        if($channelID == $this->getPrivateChannelID()) {
            return $this->getPrivateChannelName();
        }
        $userName = $this->getNameFromID($channelID-$this->getConfig('privateChannelDiff'));
        if($userName === null) {
            return null;
        }
        return $this->getPrivateChannelName($userName);
    }

    function getChannelName() {
        return $this->getChannelNameFromChannelID($this->getChannel());
    }

    function getPrivateChannelName($userName=null) {
        if($userName === null) {
            $userName = $this->getUserName();
        }
        return $this->getConfig('privateChannelPrefix').$userName.$this->getConfig('privateChannelSuffix');
    }

    function getPrivateChannelID($userID=null) {
        if($userID === null) {
            $userID = $this->getUserID();
        }
        return $userID + $this->getConfig('privateChannelDiff');
    }

    function getPrivateMessageID($userID=null) {
        if($userID === null) {
            $userID = $this->getUserID();
        }
        return $userID + $this->getConfig('privateMessageDiff');
    }

    function isAllowedToSendPrivateMessage() {
        if($this->getConfig('allowPrivateMessages') || $this->getUserRole() == AJAX_CHAT_ADMIN) {
            return true;
        }
        return false;
    }

    function isAllowedToCreatePrivateChannel() {
        if($this->getConfig('allowPrivateChannels')) {
            switch($this->getUserRole()) {
                case DONATOR:
                    return true;
                case BOTS:
                    return true;
                case PURPLE:
                    return true;
                case CMOD:
                    return true;
                case AJAX_CHAT_MODERATOR:
                    return true;
                case AJAX_CHAT_ADMIN:
                    return true;
                default:
                    return false;
            }
        }
        return false;
    }

    function isAllowedToListHiddenUsers() {
        // Hidden users are users within private or restricted channels:
        switch($this->getUserRole()) {
            case CMOD:
                return true;
            case PURPLE:
                return true;
            case AJAX_CHAT_MODERATOR:
                return true;
            case AJAX_CHAT_ADMIN:
                return true;
            default:
                return false;
        }
    }

    function isUserOnline($userID=null) {
        $userID = ($userID === null) ? $this->getUserID() : $userID;
        $userDataArray = $this->getOnlineUsersData(null,'userID',$userID);
        if($userDataArray && count($userDataArray) > 0) {
            return true;
        }
        return false;
    }

    function isUserNameInUse($userName=null) {
        $userName = ($userName === null) ? $this->getUserName() : $userName;
        $userDataArray = $this->getOnlineUsersData(null,'userName',$userName);
        if($userDataArray && count($userDataArray) > 0) {
            return true;
        }
        return false;
    }

    function isUserBanned($userName, $userID=null, $ip=null) {
        if($userID !== null) {
            $bannedUserDataArray = $this->getBannedUsersData('userID',$userID);
            if($bannedUserDataArray && isset($bannedUserDataArray[0])) {
                return true;
            }
        }
        if($ip !== null) {
            $bannedUserDataArray = $this->getBannedUsersData('ip',$ip);
            if($bannedUserDataArray && isset($bannedUserDataArray[0])) {
                return true;
            }
        }
        $bannedUserDataArray = $this->getBannedUsersData('userName',$userName);
        if($bannedUserDataArray && isset($bannedUserDataArray[0])) {
            return true;
        }
        return false;
    }

    function isMaxUsersLoggedIn() {
        if(count($this->getOnlineUsersData()) >= $this->getConfig('maxUsersLoggedIn')) {
            return true;
        }
        return false;
    }

    function validateChannel($channelID) {
        if($channelID === null) {
            return false;
        }
        // Return true for normal channels the user has acces to:
        if(in_array($channelID, $this->getChannels())) {
            return true;
        }
        // Return true if the user is allowed to join his own private channel:
        if($channelID == $this->getPrivateChannelID() && $this->isAllowedToCreatePrivateChannel()) {
            return true;
        }
        // Return true if the user has been invited to a restricted or private channel:
        if(in_array($channelID, $this->getInvitations())) {
            return true;
        }
        // No valid channel, return false:
        return false;
    }

    function createGuestUserName() {
        $maxLength =    $this->getConfig('userNameMaxLength')
                        - $this->stringLength($this->getConfig('guestUserPrefix'))
                        - $this->stringLength($this->getConfig('guestUserSuffix'));

        // seed with microseconds since last "whole" second:
        mt_srand((double)microtime()*1000000);

        // Create a random userName using numbers between 100000 and 999999:
        $userName = substr(mt_rand(100000, 999999), 0, $maxLength);

        return $this->getConfig('guestUserPrefix').$userName.$this->getConfig('guestUserSuffix');
    }

    // Guest userIDs must not interfere with existing userIDs and must be lower than privateChannelDiff:
    function createGuestUserID() {
        // seed with microseconds since last "whole" second:
        mt_srand((double)microtime()*1000000);

        return mt_rand($this->getConfig('minGuestUserID'), $this->getConfig('privateChannelDiff')-1);
    }

    function getGuestUser() {
        if(!$this->getConfig('allowGuestLogins'))
            return null;

        if($this->getConfig('allowGuestUserName')) {
            $maxLength =    $this->getConfig('userNameMaxLength')
                            - $this->stringLength($this->getConfig('guestUserPrefix'))
                            - $this->stringLength($this->getConfig('guestUserSuffix'));

            // Trim guest userName:
            $userName = $this->trimString($this->getRequestVar('userName'), null, $maxLength, true, true);

            // If given userName is invalid, create one:
            if(!$userName) {
                $userName = $this->createGuestUserName();
            } else {
                // Add the guest users prefix and suffix to the given userName:
                $userName = $this->getConfig('guestUserPrefix').$userName.$this->getConfig('guestUserSuffix');
            }
        } else {
            $userName = $this->createGuestUserName();
        }

        $userData = array();
        $userData['userID'] = $this->createGuestUserID();
        $userData['userName'] = $userName;
        $userData['userRole'] = AJAX_CHAT_GUEST;
        return $userData;
    }

    function getCustomVar($key) {
        if(!isset($this->_customVars))
            $this->_customVars = array();
        if(!isset($this->_customVars[$key]))
            return null;
        return $this->_customVars[$key];
    }

    function setCustomVar($key, $value) {
        if(!isset($this->_customVars))
            $this->_customVars = array();
        $this->_customVars[$key] = $value;
    }

    // Override to replace custom template tags:
    // Return the replacement for the given tag (and given tagContent)
    function replaceCustomTemplateTags($tag, $tagContent) {
        return null;
    }

    // Override to initialize custom configuration settings:
    function initCustomConfig() {
    }

    // Override to add custom request variables:
    // Add values to the request variables array: $this->_requestVars['customVariable'] = null;
    function initCustomRequestVars() {
    }

    // Override to add custom session code right after the session has been started:
    function initCustomSession() {
    }

    // Override, to parse custom info requests:
    // $infoRequest contains the current info request
    // Add info responses using the method addInfoMessage($info, $type)
    function parseCustomInfoRequest($infoRequest) {
    }

    // Override to replace custom text:
    // Return replaced text
    // $text contains the whole message
    function replaceCustomText(&$text) {
        return $text;
    }

    // Override to add custom commands:
    // Return true if a custom command has been successfully parsed, else false
    // $text contains the whole message, $textParts the message split up as words array
    function parseCustomCommands($text, $textParts) {
        return false;
    }

    // Override to perform custom actions on new messages:
    // Return true if message may be inserted, else false
    // $text contains the whole message
    function onNewMessage($text) {
        return true;
    }

    // Override to perform custom actions on new messages:
    // Method to set the style cookie depending on user data
    function setStyle() {
    }

    // Override:
    // Returns true if the userID of the logged in user is identical to the userID of the authentication system
    // or the user is authenticated as guest in the chat and the authentication system
    function revalidateUserID() {
        return true;
    }

    // Override:
    // Returns an associative array containing userName, userID and userRole
    // Returns null if login is invalid
    function getValidLoginUserData() {
        // Check if we have a valid registered user:
        if(false) {
            // Here is the place to check user authentication
        } else {
            // Guest users:
            return $this->getGuestUser();
        }
    }

    // Override:
    // Store the channels the current user has access to
    // Make sure channel names don't contain any whitespace
    function &getChannels() {
        if($this->_channels === null) {
            $this->_channels = $this->getAllChannels();
        }
        return $this->_channels;
    }

    // Override:
    // Store all existing channels
    // Make sure channel names don't contain any whitespace
    function &getAllChannels() {
        if($this->_allChannels === null) {
            $this->_allChannels = array();

            // Default channel, public to everyone:
            $this->_allChannels[$this->trimChannelName($this->getConfig('defaultChannelName'))] = $this->getConfig('defaultChannelID');
        }
        return $this->_allChannels;
    }

}
