From 358946b06a49b084d1aee21dea4b67b10a20d96f Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Wed, 5 Jan 2011 23:13:58 +0000 Subject: [PATCH] fixed not working default param of null for _cut_bytes($data,$offset,$len=null), thought it is NOT used without 3. parameter so far --- .../inc/class.egw_session_memcache.inc.php | 292 ++++++++++++++++++ .../inc/class.global_stream_wrapper.inc.php | 146 +++++++++ 2 files changed, 438 insertions(+) create mode 100644 phpgwapi/inc/class.egw_session_memcache.inc.php create mode 100644 phpgwapi/inc/class.global_stream_wrapper.inc.php diff --git a/phpgwapi/inc/class.egw_session_memcache.inc.php b/phpgwapi/inc/class.egw_session_memcache.inc.php new file mode 100644 index 0000000000..925672e8eb --- /dev/null +++ b/phpgwapi/inc/class.egw_session_memcache.inc.php @@ -0,0 +1,292 @@ + 1MB. This handler splits the session-data + * in 1MB chunk, so memcache can handle them. For the first chunk we use an identical + * key (just the session-id) as the original memcache session handler. For the further + * chunks we add -2, -3, ... so other code (eg. the SyncML code from Horde) can + * open the session, if it's size is < 1MB. + * + * To enable it, you need to set session.save_handler to 'memcache', + * session.save_path to 'tcp://host:port[,tcp://host2:port,...]', + * as you have to do it with the original handler PLUS adding the following + * to your header.inc.php: + * + * $GLOBALS['egw_info']['server']['session_handler'] = 'egw_session_memcache'; + * + * @link http://www.egroupware.org + * @author Ralf Becker + * @package api + * @subpackage session + * @copyright (c) 2007-8 by Ralf Becker + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @version $Id$ + */ + +// needed for check_load_extension (session-handler gets included before regular include via the header.inc.php) +require_once(EGW_API_INC.'/common_functions.inc.php'); + +/** + * File based php sessions or all other build in handlers configures via session_module_name() or php.ini: session.save_handler + * + * Contains static methods to list or count 'files' sessions (does not work under Debian, were session.save_path is not searchable!) + */ +class egw_session_memcache +{ + /** + * Debug level: 0 = none, 1 = some, 2 = all + */ + const DEBUG = 0; + /** + * Instance of Memcache + * + * @var Memcache + */ + private static $memcache; + + /** + * are the string functions overloaded by their mbstring variants + * + * @var boolean + */ + private static $mbstring_func_overload; + + /** + * Initialise the session-handler (session_set_save_handler()), if necessary + */ + public static function init_session_handler() + { + self::$mbstring_func_overload = @extension_loaded('mbstring') && (ini_get('mbstring.func_overload') & 2); + + // session needs to be closed before objects get destroyed, as this session-handler is an object ;-) + register_shutdown_function('session_write_close'); + } + + /** + * Open session + * + * @param string $save_path + * @param string $session_name + * @return boolean + */ + public static function open($save_path, $session_name) + { + check_load_extension('memcache',true); // true = throw exception if not loadable + + self::$memcache = new Memcache; + foreach(explode(',',$save_path) as $path) + { + $parts = parse_url($path); + self::$memcache->addServer($parts['host'],$parts['port']); // todo parse query + } + return true; + } + + /** + * Close session + * + * @return boolean + */ + public static function close() + { + return is_object(self::$memcache) ? self::$memcache->close() : false; + } + + /** + * Size of a single memcache junk + * + * 1024*1024 is too big, maybe some account-info needs to be added + */ + const MEMCACHED_MAX_JUNK = 1024000; + + /** + * Read session data + * + * According to a commentary on php.net (session_set_save_handler) this function has to return + * a string, if the session-id is NOT found, not false. Returning false terminates the script + * with a fatal error! + * + * @param string $id + * @return string|boolean false on error, '' for not found session otherwise session data + */ + public static function read($id) + { + if (self::DEBUG > 0) error_log("\n memcache ".$_SERVER["REQUEST_TIME"]." READ start $id:"); + + if (!self::_acquire_and_wait($id)) return false; + + for($data='',$n=0; ($read = self::$memcache->get($id.($n?'-'.$n:''))); ++$n) + { + if (self::DEBUG > 0) error_log("\n memcache ".$_SERVER["REQUEST_TIME"]." read $id:$n:".print_r(self::_bytes($read),true)); + $data .= $read; + } + self::_release($id); + + return $data; + } + + /** + * Write session data + * + * @param string $id + * @param string $sess_data + * @return boolean + */ + public static function write($id, $sess_data) + { + $lifetime = (int)ini_get('session.gc_maxlifetime'); + + if ($id == 'no-session') + { + return true; // no need to save + } + // give anon sessions only a lifetime of 10min + if (is_object($GLOBALS['egw']->session) && $GLOBALS['egw']->session->session_flags == 'A' || + $GLOBALS['egw_info']['flags']['currentapp'] == 'groupdav') + { + $lifetime = 600; + } + if (self::DEBUG > 0) error_log("\n memcache ".$_SERVER["REQUEST_TIME"]." WRITE start $id:"); + + if (!self::_acquire_and_wait($id)) return false; + + for($n=$i=0,$len=self::_bytes($sess_data); $i < $len; $i += self::MEMCACHED_MAX_JUNK,++$n) + { + if (self::DEBUG > 1) error_log("\n memcache ".$_SERVER["REQUEST_TIME"]." in :$n write $id:$i:".print_r(self::_bytes($sess_data),true)); + + if (!self::$memcache->set($id.($n?'-'.$n:''),self::_cut_bytes($sess_data,$i,self::MEMCACHED_MAX_JUNK),0,$lifetime)) { + self::_release($id); + return false; + } + } + if (self::DEBUG > 0) error_log("\n memcache ".$_SERVER["REQUEST_TIME"]." DELETE :$n"); + for($n=$n; self::$memcache->delete($id.($n?'-'.$n:'')); ++$n) ; + + self::_release($id); + + return true; + } + + /** + * semaphore to gard against conflicting writes, destroying the session-data + * + * @param string $id + * @return boolean + */ + private static function _acquire_and_wait($id) + { + if (self::DEBUG > 0) error_log("\n memcache ".$_SERVER["REQUEST_TIME"]." ACQUIRE :$id"); + + $i=0; + // Acquire lock for 3 seconds, after that i should have done my job + while(!self::$memcache->add($id.'-lock',1,0,3)) + { + if (self::DEBUG > 0) error_log("\n memcache ".$_SERVER["REQUEST_TIME"]." ACQUIRE Lock Loop :$id:$i"); + usleep(100000); + $i++; + if ($i > 40) + { + if (self::DEBUG > 1) error_log("\n memcache ".print_r(getenv('HOSTNAME'),true).$_SERVER["REQUEST_TIME"]." blocked :$id"); + // Could not acquire lock after 3 seconds, Continue, and pretend the locking process get stuck + // return false;A + break; + } + } + if($i > 1) + { + if (self::DEBUG > 0) error_log("\n memcache ".$_SERVER["REQUEST_TIME"]." ACQUIRE LOOP $i :$id"); + } + if (self::DEBUG > 0) error_log("\n memcache ".print_r(getenv('HOSTNAME'),true).$_SERVER["REQUEST_TIME"]." Lock ACQUIRED $i:$id"); + + return true; + } + + /** + * Release semaphore + * + * @param string $id + * @return boolean + */ + private static function _release($id) + { + if (self::DEBUG > 0) error_log("\n memcache ".$_SERVER["REQUEST_TIME"]." RELEASE :$id"); + + return self::$memcache->delete($id.'-lock'); + } + + /** + * mbstring.func_overload safe strlen + * + * @param string $data + * @return int + */ + private static function _bytes(&$data) + { + return self::$mbstring_func_overload ? mb_strlen($data,'ascii') : strlen($data); + } + + /** + * mbstring.func_overload safe substr + * + * @param string $data + * @param int $offset + * @param int $len + * @return string + */ + private static function _cut_bytes(&$data,$offset,$len=null) + { + if (self::DEBUG > 1) error_log("\n memcache in cutbyte mb $id:$n:".print_r(mb_substr($data,$offset,$len,'ascii'),true)); + if (self::DEBUG > 1) error_log("\n memcache in cutbyte norm $id:$n:".print_r(substr($data,$offset,$len),true)); + + if (is_null($len)) + { + return self::$mbstring_func_overload ? mb_substr($data,$offset,self::_bytes($data),'ascii') : substr($data,$offset); + } + return self::$mbstring_func_overload ? mb_substr($data,$offset,$len,'ascii') : substr($data,$offset,$len); + } + + /** + * Destroy a session + * + * @param string $id + * @return boolean + */ + public static function destroy($id) + { + if (!self::_acquire_and_wait($id)) return false; + + for($n=0; self::$memcache->delete($id.($n?'-'.$n:'')); ++$n) + { + if (self::DEBUG > 0) error_log("******* memcache destroy $id:$n:"); + } + self::_release($id); + + return $n > 0; + } + + /** + * Run garbade collection + * + * @param int $maxlifetime + */ + public static function gc($maxlifetime) + { + // done by memcached itself + } +} + +function init_session_handler() +{ + $ses = 'egw_session_memcache'; + $ret = session_set_save_handler( + array($ses,'open'), + array($ses,'close'), + array($ses,'read'), + array($ses,'write'), + array($ses,'destroy'), + array($ses,'gc')); + if (!$ret) error_log(__METHOD__.'() session_set_save_handler(...)='.(int)$ret.', session_module_name()='.session_module_name().' *******************************'); +} +init_session_handler(); diff --git a/phpgwapi/inc/class.global_stream_wrapper.inc.php b/phpgwapi/inc/class.global_stream_wrapper.inc.php new file mode 100644 index 0000000000..1c362f1660 --- /dev/null +++ b/phpgwapi/inc/class.global_stream_wrapper.inc.php @@ -0,0 +1,146 @@ +array('key2'=>$string)); + */ +class global_stream_wrapper +{ + private $pos; + private $stream; + private $name; + + public function stream_open($path, $mode, $options, &$opened_path) + { + $this->stream = &$GLOBALS[$this->name=parse_url($path,PHP_URL_HOST)]; + $this->pos = 0; + if (!is_string($this->stream)) return false; + return true; + } + + public function stream_read($count) + { + $ret = self::_cut_bytes($this->stream, $this->pos, $count); + //error_log(__METHOD__."($count) this->pos=$this->pos, self::_bytes(this->stream)=".self::_bytes($this->stream)." self::_bytes(ret)=".self::_bytes($ret)); + $this->pos += self::_bytes($ret); + return $ret; + } + + public function stream_write($data) + { + $l=self::_bytes($data); + $this->stream = + self::_cut_bytes($this->stream, 0, $this->pos) . + $data . + self::_cut_bytes($this->stream, $this->pos += $l); + return $l; + } + + public function stream_tell() + { + return $this->pos; + } + + public function stream_eof() + { + return $this->pos >= self::_bytes($this->stream); + } + + public function stream_seek($offset, $whence) + { + $l=self::_bytes($this->stream); + switch ($whence) + { + case SEEK_SET: $newPos = $offset; break; + case SEEK_CUR: $newPos = $this->pos + $offset; break; + case SEEK_END: $newPos = $l + $offset; break; + default: return false; + } + $ret = ($newPos >=0 && $newPos <=$l); + if ($ret) $this->pos=$newPos; + return $ret; + } + + public function stream_stat() + { + if (!isset($this->stream)) + { + return false; + } + return array( + 'ino' => md5($this->name), + 'name' => $this->name, + 'mode' => 0100000, + 'size' => bytes($this->stream), + 'uid' => 0, + 'gid' => 0, + 'mtime' => 0, + 'ctime' => 0, + 'nlink' => 1, + ); + } + + /** + * are the string functions overloaded by their mbstring variants + * + * @var boolean + */ + private static $mbstring_func_overload; + + /** + * mbstring.func_overload safe strlen + * + * @param string &$data + * @return int + */ + private static function _bytes(&$data) + { + return self::$mbstring_func_overload ? mb_strlen($data,'ascii') : strlen($data); + } + + /** + * mbstring.func_overload safe substr + * + * @param string &$data + * @param int $offset + * @param int $len + * @return string + */ + private static function _cut_bytes(&$data,$offset,$len=null) + { + if (is_null($len)) + { + return self::$mbstring_func_overload ? mb_substr($data,$offset,self::_bytes($data),'ascii') : substr($data,$offset); + } + return self::$mbstring_func_overload ? mb_substr($data,$offset,$len,'ascii') : substr($data,$offset,$len); + } + + /** + * Init the (static parts) of the stream-wrapper + * + */ + public static function init() + { + self::$mbstring_func_overload = @extension_loaded('mbstring') && (ini_get('mbstring.func_overload') & 2); + + stream_wrapper_register('global', 'global_stream_wrapper'); + } +} +global_stream_wrapper::init();