* * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the * Free Software Foundation, 51 Franklin St, Fifth Floor, * Boston, MA 02110-1301 USA */ require_once dirname(__FILE__)."/pfccontainerinterface.class.php"; require_once dirname(__FILE__)."/pfcurlprocessing.php"; /** * pfcContainer is an abstract class which define interface * to be implemented by concrete container (example: File) * * @author Stephane Gully * @abstract */ class pfcContainer extends pfcContainerInterface { var $_container = null; // contains the concrete container instance var $_usememorycache = true; function &Instance($type = 'File', $usememorycache = true) { static $i; if (!isset($i)) $i = new pfcContainer($type, $usememorycache); return $i; } function pfcContainer($type = 'File', $usememorycache = true) { pfcContainerInterface::pfcContainerInterface(); $this->_usememorycache = $usememorycache; $type = strtolower($type); // create the concrete container instance require_once dirname(__FILE__)."/containers/".$type.".class.php"; $container_classname = "pfcContainer_".$type; $this->_container = new $container_classname(); } function getDefaultConfig() { if ($this->_container) return $this->_container->getDefaultConfig(); else return array(); } function init(&$c) { if ($this->_container) return $this->_container->init($c); } /** * Create (connect/join) the nickname into the server or the channel locations * Notice: the caller must take care to update all channels the users joined (use stored channel list into metadata) * @param $chan if NULL then create the user on the server (connect), otherwise create the user on the given channel (join) * @param $nick the nickname to create * @param $nickid is the corresponding nickname id (taken from session) */ /* function createNick($chan, $nick, $nickid) { $c =& pfcGlobalConfig::Instance(); if ($nick == '') user_error('pfcContainer::createNick nick is empty', E_USER_ERROR); if ($nickid == '') user_error('pfcContainer::createNick nickid is empty', E_USER_ERROR); if ($chan == NULL) $chan = 'SERVER'; $this->setMeta("nickid-to-metadata", $nickid, 'nick', $nick); $this->setMeta("metadata-to-nickid", 'nick', $this->encode($nick), $nickid); $this->setMeta("nickid-to-channelid", $nickid, $this->encode($chan)); $this->setMeta("channelid-to-nickid", $this->encode($chan), $nickid); // update the SERVER channel if ($chan != 'SERVER') $this->updateNick($nickid); return true; } */ function createNick($nickid, $nick) { $c =& pfcGlobalConfig::Instance(); if ($nick == '') user_error('pfcContainer::createNick nick is empty', E_USER_ERROR); if ($nickid == '') user_error('pfcContainer::createNick nickid is empty', E_USER_ERROR); $this->setMeta("nickid-to-metadata", $nickid, 'nick', $nick); $this->setMeta("metadata-to-nickid", 'nick', $this->encode($nick), $nickid); return true; } function joinChan($nickid, $chan) { $c =& pfcGlobalConfig::Instance(); if ($nickid == '') user_error('pfcContainer::joinChan nickid is empty', E_USER_ERROR); if ($chan == NULL) $chan = 'SERVER'; $this->setMeta("nickid-to-channelid", $nickid, $this->encode($chan)); $this->setMeta("channelid-to-nickid", $this->encode($chan), $nickid); // update the SERVER channel if ($chan == 'SERVER') $this->updateNick($nickid); return true; } /** * Remove (disconnect/quit) the nickname from the server or from a channel * Notice: when a user quit, the caller must take care removeNick from each channels ('SERVER' included) * This function takes care to remove all users metadata when he his disconnected from all channels * @param $chan if NULL then remove the user from the 'SERVER' channel, otherwise just remove the user from the given channel (quit) * @param $nickid the nickname id to remove * @return array which contains removed user infos ('nickid', 'nick', 'timestamp') */ function removeNick($chan, $nickid) { $c =& pfcGlobalConfig::Instance(); if ($chan == NULL) $chan = 'SERVER'; $deleted_user = array(); $deleted_user["nick"] = array(); $deleted_user["nickid"] = array(); $deleted_user["timestamp"] = array(); if (!$nickid) return $deleted_user; $timestamp = $this->getMeta("channelid-to-nickid", $this->encode('SERVER'), $nickid); if (count($timestamp["timestamp"]) == 0) return $deleted_user; $timestamp = $timestamp["timestamp"][0]; $deleted_user["nick"][] = $this->getNickname($nickid); $deleted_user["nickid"][] = $nickid; $deleted_user["timestamp"][] = $timestamp; // remove the nickid from the channel list $this->rmMeta('channelid-to-nickid', $this->encode($chan), $nickid); $this->rmMeta('nickid-to-channelid', $nickid, $this->encode($chan)); // if the user is the last one to quit this room, // and this room is not a default room, // then clean the room history $channels = array(); foreach($c->channels as $cc) $channels[] = 'ch_'.$cc; // @todo clean this piece of code when the chan and chanid will be refactored if (!in_array($chan, $channels)) { $ret = $this->getOnlineNick($chan); if (count($ret['nickid']) == 0) { $this->rmMeta('channelid-to-msg', $this->encode($chan)); $this->rmMeta('channelid-to-msgid', $this->encode($chan)); } } // get the current user's channels list $channels = $this->getMeta("nickid-to-channelid",$nickid); $channels = $channels["value"]; // no more joined channel, just remove the user's metadata if (count($channels) == 0) { // remove the nickname to nickid correspondance $this->rmMeta('metadata-to-nickid', 'nick', $this->encode($this->getNickname($nickid))); // remove disconnected nickname metadata $this->rmMeta('nickid-to-metadata', $nickid); // remove users commands in queue $this->rmMeta("nickid-to-cmdtoplay", $nickid); $this->rmMeta("nickid-to-cmdtoplayid", $nickid); } return $deleted_user; } /** * Store/update the alive user status on the 'SERVER' channel * The default File container will just touch (update the date) of the nickname file in the 'SERVER' channel. * @param $nickid the nickname id to keep alive */ function updateNick($nickid) { $c =& pfcGlobalConfig::Instance(); $chan = 'SERVER'; $this->setMeta("nickid-to-channelid", $nickid, $this->encode($chan)); $this->setMeta("channelid-to-nickid", $this->encode($chan), $nickid); return true; } /** * Change the user's nickname * As nickname value are stored in user's metadata, this function just update the 'nick' metadata * @param $newnick * @param $oldnick * @return true on success, false on failure (if the oldnick doesn't exists) */ function changeNick($newnick, $oldnick) { $c =& pfcGlobalConfig::Instance(); $oldnickid = $this->getNickId($oldnick); $newnickid = $this->getNickId($newnick); if ($oldnickid == "") return false; // the oldnick must be connected if ($newnickid != "") return false; // the newnick must not be inuse // remove the oldnick to oldnickid correspondance $this->rmMeta("metadata-to-nickid", 'nick', $this->encode($oldnick)); // update the nickname $this->setMeta("nickid-to-metadata", $oldnickid, 'nick', $newnick); $this->setMeta("metadata-to-nickid", 'nick', $this->encode($newnick), $oldnickid); return true; } /** * Returns the nickid corresponding to the given nickname * The nickid is a unique id used to identify a user (generated from the browser sessionid) * The nickid is stored in the container when createNick is called. * @param $nick * @return string the nick id */ function getNickId($nick) { $nickid = $this->getMeta("metadata-to-nickid", 'nick', $this->encode($nick), true); $nickid = isset($nickid["value"][0]) ? $nickid["value"][0] : ""; return $nickid; } /** * Returns the nickname corresponding the the given nickid * @param $nickid * @return string the corresponding nickname */ function getNickname($nickid) { $nick = $this->getMeta("nickid-to-metadata", $nickid, 'nick', true); $nick = isset($nick["value"][0]) ? $this->decode($nick["value"][0]) : ""; return $nick; } /** * Remove (disconnect/quit) the timeouted nicknames * Notice: this function will remove all nicknames which are not uptodate from all his joined channels * @param $timeout * @return array("nickid"=>array("nickid1", ...),"timestamp"=>array(timestamp1, ...)) contains all disconnected nickids and there timestamp */ function removeObsoleteNick($timeout) { $c =& pfcGlobalConfig::Instance(); $deleted_user = array('nick'=>array(), 'nickid'=>array(), 'timestamp'=>array(), 'channels'=>array()); $ret = $this->getMeta("channelid-to-nickid", $this->encode('SERVER')); for($i = 0; $i ($timestamp+$timeout/1000) && $nickid) // user will be disconnected after 'timeout' secondes of inactivity { // get the current user's channels list $channels = array(); $ret2 = $this->getMeta("nickid-to-channelid",$nickid); foreach($ret2["value"] as $userchan) { $userchan = $this->decode($userchan); if ($userchan != 'SERVER') { // disconnect the user from each joined channels $this->removeNick($userchan, $nickid); $channels[] = $userchan; } } // now disconnect the user from the server // (order is important because the SERVER channel has timestamp informations) $du = $this->removeNick('SERVER', $nickid); $channels[] = 'SERVER'; $deleted_user["nick"] = array_merge($deleted_user["nick"], $du["nick"]); $deleted_user["nickid"] = array_merge($deleted_user["nickid"], $du["nickid"]); $deleted_user["timestamp"] = array_merge($deleted_user["timestamp"], $du["timestamp"]); $deleted_user["channels"] = array_merge($deleted_user["channels"], array($channels)); } } return $deleted_user; } /** * Returns the nickname list on the given channel or on the whole server * @param $chan if NULL then returns all connected user, otherwise just returns the channel nicknames * @return array("nickid"=>array("nickid1", ...),"timestamp"=>array(timestamp1, ...)) contains the nickid list with the associated timestamp (laste update time) */ function getOnlineNick($chan) { $c =& pfcGlobalConfig::Instance(); if ($chan == NULL) $chan = 'SERVER'; $online_user = array('nick'=>array(),'nickid'=>array(),'timestamp'=>array()); $ret = $this->getMeta("channelid-to-nickid", $this->encode($chan)); for($i = 0; $igetMeta("channelid-to-nickid", $this->encode('SERVER'), $nickid); if (count($timestamp['timestamp']) == 0) continue; $timestamp = $timestamp['timestamp'][0]; $online_user["nick"][] = $this->getNickname($nickid); $online_user["nickid"][] = $nickid; $online_user["timestamp"][] = $timestamp; } return $online_user; } /** * Returns returns a positive number if the nick is online in the given channel * @param $chan if NULL then check if the user is online on the server, otherwise check if the user has joined the channel * @return false if the user is off line, true if the user is online */ function isNickOnline($chan, $nickid) { if (!$nickid) return false; if ($chan == NULL) $chan = 'SERVER'; $ret = $this->getMeta("channelid-to-nickid", $this->encode($chan), $nickid); return (count($ret['timestamp']) > 0); } /** * Write a command to the given channel or to the server * Notice: a message is very generic, it can be a misc command (notice, me, ...) * @param $chan if NULL then write the message on the server, otherwise just write the message on the channel message pool * @param $nick is the sender nickname * @param $cmd is the command name (ex: "send", "nick", "kick" ...) * @param $param is the command' parameters (ex: param of the "send" command is the message) * @return $msg_id the created message identifier */ function write($chan, $nick, $cmd, $param) { $c =& pfcGlobalConfig::Instance(); if ($chan == NULL) $chan = 'SERVER'; $msgid = $this->_requestMsgId($chan); // format message $data = "\n"; $data .= $msgid."\t"; $data .= time()."\t"; $data .= $nick."\t"; $data .= $cmd."\t"; $data .= $param; // write message $this->setMeta("channelid-to-msg", $this->encode($chan), $msgid, $data); // delete the obsolete message $old_msgid = $msgid - $c->max_msg - 20; if ($old_msgid > 0) $this->rmMeta("channelid-to-msg", $this->encode($chan), $old_msgid); return $msgid; } /** * Read the last posted commands from a channel or from the server * Notice: the returned array is ordered by id * @param $chan if NULL then read from the server, otherwise read from the given channel * @param $from_id read all message with a greater id * @return array() contains the formated command list */ function read($chan, $from_id) { $c =& pfcGlobalConfig::Instance(); if ($chan == NULL) $chan = 'SERVER'; // read new messages content + parse content $new_from_id = $this->getLastId($chan); $datalist = array(); for ( $mid = $from_id; $mid <= $new_from_id; $mid++ ) { $line = $this->getMeta("channelid-to-msg", $this->encode($chan), $mid, true); $line = $line["value"][0]; if ($line != "" && $line != "\n") { $formated_line = explode( "\t", $line ); $data = array(); $data["id"] = trim($formated_line[0]); $data["timestamp"] = $formated_line[1]; $data["sender"] = $formated_line[2]; $data["cmd"] = $formated_line[3]; // convert URLs to html $data["param"] = $formated_line[4]; $datalist[$data["id"]] = $data; } } return array("data" => $datalist, "new_from_id" => $new_from_id+1 ); } /** * Returns the last message id * Notice: the default file container just returns the messages.index file content * @param $chan if NULL then read if from the server, otherwise read if from the given channel * @return int is the last posted message id */ function getLastId($chan) { if ($chan == NULL) $chan = 'SERVER'; $lastmsgid = $this->getMeta("channelid-to-msgid", $this->encode($chan), 'lastmsgid', true); if (count($lastmsgid["value"]) == 0) $lastmsgid = 0; else $lastmsgid = $lastmsgid["value"][0]; return $lastmsgid; } /** * Return a unique id. Each time this function is called, the last id is incremented. * used internaly * @private */ function _requestMsgId($chan) { if ($chan == NULL) $chan = 'SERVER'; $lastmsgid = $this->incMeta("channelid-to-msgid", $this->encode($chan), 'lastmsgid'); if (count($lastmsgid["value"]) == 0) $lastmsgid = 0; else $lastmsgid = $lastmsgid["value"][0]; return $lastmsgid; } /** * Remove all created data for this server (identified by serverid) * Notice: for the default File container, it's just a recursive directory remove */ function clear() { $this->rmMeta(NULL); } function getAllUserMeta($nickid) { $result = array(); $ret = $this->getMeta("nickid-to-metadata", $nickid); foreach($ret["value"] as $k) $result[$k] = $this->getUserMeta($nickid, $k); // $result['chanid'] = $this->getMeta("nickid-to-channelid", $nickid); // $result['chanid'] = $result['chanid']['value']; return $result; } function getUserMeta($nickid, $key = NULL) { $ret = $this->getMeta("nickid-to-metadata", $nickid, $key, true); return isset($ret['value'][0]) ? $ret['value'][0] : NULL; } function setUserMeta($nickid, $key, $value) { $ret = $this->setMeta("nickid-to-metadata", $nickid, $key, $value); return $ret; } function getCmdMeta($nickid, $key = NULL) { $ret = $this->getMeta("nickid-to-cmdtoplay", $nickid, $key, true); return $ret['value']; } function setCmdMeta($nickid, $key, $value) { $ret = $this->setMeta("nickid-to-cmdtoplay", $nickid, $key, $value); return $ret; } function getAllChanMeta($chan) { $result = array(); $ret = $this->getMeta("channelid-to-metadata", $this->encode($chan)); foreach($ret["value"] as $k) $result[$k] = $this->getChanMeta($chan, $k); return $result; } function getChanMeta($chan, $key = NULL) { $ret = $this->getMeta("channelid-to-metadata", $this->encode($chan), $key, true); return isset($ret['value'][0]) ? $ret['value'][0] : NULL; } function setChanMeta($chan, $key, $value) { $ret = $this->setMeta("channelid-to-metadata", $this->encode($chan), $key, $value); return $ret; } var $_cache = array(); /** * Write a meta data value identified by a group / subgroup / leaf [with a value] * As an example in the default file container this arborescent structure is modelised by simple directories * group1/subgroup1/leaf1 * /leaf2 * /subgroup2/... * Each leaf can contain or not a value. * However each leaf and each group/subgroup must store the lastmodified time (timestamp). * @param $group root arborescent element * @param $subgroup is the root first child which contains leafs * @param $leaf is the only element which can contain values * @param $leafvalue NULL means the leaf will not contain any values * @return 1 if the old leaf has been overwritten, 0 if a new leaf has been created */ function setMeta($group, $subgroup, $leaf, $leafvalue = NULL) { $ret = $this->_container->setMeta($group, $subgroup, $leaf, $leafvalue); if ($this->_usememorycache) { // store the modifications in the cache if (isset($this->_cache[$group]['value']) && !in_array($subgroup, $this->_cache[$group]['value'])) { $this->_cache[$group]['value'][] = $subgroup; $this->_cache[$group]['timestamp'][] = time(); } if (isset($this->_cache[$group]['childs'][$subgroup]['value']) && !in_array($leaf, $this->_cache[$group]['childs'][$subgroup]['value'])) { $this->_cache[$group]['childs'][$subgroup]['value'][] = $leaf; $this->_cache[$group]['childs'][$subgroup]['timestamp'][] = time(); } $this->_cache[$group]['childs'][$subgroup]['childs'][$leaf]['value'] = array($leafvalue); $this->_cache[$group]['childs'][$subgroup]['childs'][$leaf]['timestamp'] = array(time()); } return $ret; } /** * Read meta data identified by a group [/ subgroup [/ leaf]] * @param $group is mandatory, it's the arborescence's root * @param $subgroup if null then the subgroup list names are returned * @param $leaf if null then the leaf names are returned * @param $withleafvalue if set to true the leaf value will be returned * @return array which contains two subarray 'timestamp' and 'value' */ function getMeta($group, $subgroup = null, $leaf = null, $withleafvalue = false) { $ret = array('timestamp' => array(), 'value' => array()); if ($this->_usememorycache) { // check if the data exists in the cache $incache = false; if ($subgroup == null && isset($this->_cache[$group]['value'])) { $incache = true; $ret = $this->_cache[$group]; } else if ($leaf == null && isset($this->_cache[$group]['childs'][$subgroup]['value'])) { $incache = true; $ret = $this->_cache[$group]['childs'][$subgroup]; } else { if ($withleafvalue) { if (isset($this->_cache[$group]['childs'][$subgroup]['childs'][$leaf]['value'])) { $incache = true; $ret = $this->_cache[$group]['childs'][$subgroup]['childs'][$leaf]; } } else { if (isset($this->_cache[$group]['childs'][$subgroup]['childs'][$leaf]['timestamp'])) { $incache = true; $ret = $this->_cache[$group]['childs'][$subgroup]['childs'][$leaf]; } } } if ($incache) { $ret2 = array(); if (isset($ret['timestamp'])) $ret2['timestamp'] = $ret['timestamp']; if (isset($ret['value'])) $ret2['value'] = $ret['value']; return $ret2; } } // get the fresh data $ret = $this->_container->getMeta($group, $subgroup, $leaf, $withleafvalue); if ($this->_usememorycache) { // store in the cache if ($subgroup == null) { $this->_cache[$group]['value'] = $ret['value']; $this->_cache[$group]['timestamp'] = $ret['timestamp']; } else if ($leaf == null) { $this->_cache[$group]['childs'][$subgroup]['value'] = $ret['value']; $this->_cache[$group]['childs'][$subgroup]['timestamp'] = $ret['timestamp']; } else { if ($withleafvalue) $this->_cache[$group]['childs'][$subgroup]['childs'][$leaf]['value'] = $ret['value']; else unset($this->_cache[$group]['childs'][$subgroup]['childs'][$leaf]['value']); $this->_cache[$group]['childs'][$subgroup]['childs'][$leaf]['timestamp'] = $ret['timestamp']; } } return $ret; } /** * Increment a counter identified by the following path : group / subgroup / leaf * Notice: this step must be atomic in order to avoid multithread problem (don't forget to use locking features) * @param $group is mandatory * @param $subgroup is mandatory * @param $leaf is mandatory, it's the counter name * @return array which contains two subarray 'timestamp' and 'value' (value contains the incremented numeric value) */ function incMeta($group, $subgroup, $leaf) { $ret = $this->_container->incMeta($group, $subgroup, $leaf); if ($this->_usememorycache) { // store the modifications in the cache if (isset($this->_cache[$group]['value']) && !in_array($subgroup, $this->_cache[$group]['value'])) { $this->_cache[$group]['value'][] = $subgroup; $this->_cache[$group]['timestamp'][] = time(); } if (isset($this->_cache[$group]['childs'][$subgroup]['value']) && !in_array($leaf, $this->_cache[$group]['childs'][$subgroup]['value'])) { $this->_cache[$group]['childs'][$subgroup]['value'][] = $leaf; $this->_cache[$group]['childs'][$subgroup]['timestamp'][] = time(); } $this->_cache[$group]['childs'][$subgroup]['childs'][$leaf]['value'] = $ret['value']; $this->_cache[$group]['childs'][$subgroup]['childs'][$leaf]['timestamp'] = array(time()); } return $ret; } /** * Remove a meta data or a group of metadata * @param $group if null then it will remove all the possible groups (all the created metadata) * @param $subgroup if null then it will remove the $group's childs (all the subgroup contained by $group) * @param $leaf if null then it will remove all the $subgroup's childs (all the leafs contained by $subgroup) * @return true on success, false on error */ function rmMeta($group, $subgroup = null, $leaf = null) { if ($this->_usememorycache) { // remove from the cache if ($group == null) $this->_cache = array(); else if ($subgroup == null) unset($this->_cache[$group]); else if ($leaf == null) { if (isset($this->_cache[$group]['value'])) { $i = array_search($subgroup,$this->_cache[$group]['value']); if ($i !== FALSE) { unset($this->_cache[$group]['value'][$i]); unset($this->_cache[$group]['timestamp'][$i]); } } unset($this->_cache[$group]['childs'][$subgroup]); } else { if (isset($this->_cache[$group]['childs'][$subgroup]['value'])) { $i = array_search($leaf,$this->_cache[$group]['childs'][$subgroup]['value']); if ($i !== FALSE) { unset($this->_cache[$group]['childs'][$subgroup]['value'][$i]); unset($this->_cache[$group]['childs'][$subgroup]['timestamp'][$i]); } } unset($this->_cache[$group]['childs'][$subgroup]['childs'][$leaf]); } } return $this->_container->rmMeta($group, $subgroup, $leaf); } /** * In the default File container: used to encode UTF8 strings to ASCII filenames * This method can be overridden by the concrete container */ function encode($str) { return $this->_container->encode($str); } /** * In the default File container: used to decode ASCII filenames to UTF8 strings * This method can be overridden by the concrete container */ function decode($str) { return $this->_container->decode($str); } } ?>