diff --git a/etemplate/inc/class.etemplate_new.inc.php b/etemplate/inc/class.etemplate_new.inc.php index 5137308c45..7907f6d2e6 100644 --- a/etemplate/inc/class.etemplate_new.inc.php +++ b/etemplate/inc/class.etemplate_new.inc.php @@ -634,7 +634,7 @@ foreach($widgets as $app => $list) { try { - __autoload($class); + class_exists($class); // trigger autoloader } catch(Exception $e) { diff --git a/phpgwapi/inc/class.egw_vfs.inc.php b/phpgwapi/inc/class.egw_vfs.inc.php index a9e3edf99f..a2af4f07b9 100644 --- a/phpgwapi/inc/class.egw_vfs.inc.php +++ b/phpgwapi/inc/class.egw_vfs.inc.php @@ -11,2097 +11,9 @@ * @version $Id$ */ +use EGroupware\Api\Vfs; + /** - * Class containing static methods to use the new eGW virtual file system - * - * This extension of the vfs stream-wrapper allows to use the following static functions, - * which only allow access to the eGW VFS and need no 'vfs://default' prefix for filenames: - * - * - resource egw_vfs::fopen($path,$mode) like fopen, returned resource can be used with fwrite etc. - * - resource egw_vfs::opendir($path) like opendir, returned resource can be used with readdir etc. - * - boolean egw_vfs::copy($from,$to) like copy - * - boolean egw_vfs::rename($old,$new) renaming or moving a file in the vfs - * - boolean egw_vfs::mkdir($path) creating a new dir in the vfs - * - boolean egw_vfs::rmdir($path) removing (an empty) directory - * - boolean egw_vfs::unlink($path) removing a file - * - boolean egw_vfs::touch($path,$mtime=null) touch a file - * - boolean egw_vfs::stat($path) returning status of file like stat(), but only with string keys (no numerical indexes)! - * - * With the exception of egw_vfs::touch() (not yet part of the stream_wrapper interface) - * you can always use the standard php functions, if you add a 'vfs://default' prefix - * to every filename or path. Be sure to always add the prefix, as the user otherwise gains - * access to the real filesystem of the server! - * - * The two following methods can be used to persitently mount further filesystems (without editing the code): - * - * - boolean|array egw_vfs::mount($url,$path) to mount $ur on $path or to return the fstab when called without argument - * - boolean egw_vfs::umount($path) to unmount a path or url - * - * The stream wrapper interface allows to access hugh files in junks to not be limited by the - * memory_limit setting of php. To do you should pass the opened file as resource and not the content: - * - * $file = egw_vfs::fopen('/home/user/somefile','r'); - * $content = fread($file,1024); - * - * You can also attach stream filters, to eg. base64 encode or compress it on the fly, - * without the need to hold the content of the whole file in memmory. - * - * If you want to copy a file, you can use stream_copy_to_stream to do a copy of a file far bigger then - * php's memory_limit: - * - * $from = egw_vfs::fopen('/home/user/fromfile','r'); - * $to = egw_vfs::fopen('/home/user/tofile','w'); - * - * stream_copy_to_stream($from,$to); - * - * The static egw_vfs::copy() method does exactly that, but you have to do it eg. on your own, if - * you want to copy eg. an uploaded file into the vfs. - * - * egw_vfs::parse_url($url, $component=-1), egw_vfs::dirname($url) and egw_vfs::basename($url) work - * on urls containing utf-8 characters, which get NOT urlencoded in our VFS! + * @deprecated use EGroupware\Api\Vfs */ -class egw_vfs extends vfs_stream_wrapper -{ - const PREFIX = 'vfs://default'; - /** - * Readable bit, for dirs traversable - */ - const READABLE = 4; - /** - * Writable bit, for dirs delete or create files in that dir - */ - const WRITABLE = 2; - /** - * Excecutable bit, here only use to check if user is allowed to search dirs - */ - const EXECUTABLE = 1; - /** - * Name of the lock table - */ - const LOCK_TABLE = 'egw_locks'; - /** - * Current user has root rights, no access checks performed! - * - * @var boolean - */ - static $is_root = false; - /** - * Current user id, in case we ever change if away from $GLOBALS['egw_info']['user']['account_id'] - * - * @var int - */ - static $user; - /** - * Current user is an eGW admin - * - * @var boolean - */ - static $is_admin = false; - /** - * Total of last find call - * - * @var int - */ - static $find_total; - /** - * Reference to the global db object - * - * @var egw_db - */ - static $db; - - /** - * fopen working on just the eGW VFS - * - * @param string $path filename with absolute path in the eGW VFS - * @param string $mode 'r', 'w', ... like fopen - * @return resource - */ - static function fopen($path,$mode) - { - if ($path[0] != '/') - { - throw new egw_exception_assertion_failed("Filename '$path' is not an absolute path!"); - } - return fopen(self::PREFIX.$path,$mode); - } - - /** - * opendir working on just the eGW VFS: returns resource for readdir() etc. - * - * @param string $path filename with absolute path in the eGW VFS - * @return resource - */ - static function opendir($path) - { - if ($path[0] != '/') - { - throw new egw_exception_assertion_failed("Directory '$path' is not an absolute path!"); - } - return opendir(self::PREFIX.$path); - } - - /** - * dir working on just the eGW VFS: returns directory object - * - * @param string $path filename with absolute path in the eGW VFS - * @return Directory - */ - static function dir($path) - { - if ($path[0] != '/') - { - throw new egw_exception_assertion_failed("Directory '$path' is not an absolute path!"); - } - return dir(self::PREFIX.$path); - } - - /** - * scandir working on just the eGW VFS: returns array with filenames as values - * - * @param string $path filename with absolute path in the eGW VFS - * @param int $sorting_order =0 !$sorting_order (default) alphabetical in ascending order, $sorting_order alphabetical in descending order. - * @return array - */ - static function scandir($path,$sorting_order=0) - { - if ($path[0] != '/') - { - throw new egw_exception_assertion_failed("Directory '$path' is not an absolute path!"); - } - return scandir(self::PREFIX.$path,$sorting_order); - } - - /** - * copy working on just the eGW VFS - * - * @param string $from - * @param string $to - * @return boolean - */ - static function copy($from,$to) - { - $old_props = self::file_exists($to) ? self::propfind($to,null) : array(); - // copy properties (eg. file comment), if there are any and evtl. existing old properties - $props = self::propfind($from,null); - - foreach($old_props as $prop) - { - if (!self::find_prop($props,$prop)) - { - $prop['val'] = null; // null = delete prop - $props[] = $prop; - } - } - // using self::copy_uploaded() to treat copying incl. properties as atomar operation in respect of notifications - return self::copy_uploaded(self::PREFIX.$from,$to,$props,false); // false = no is_uploaded_file check! - } - - /** - * Find a specific property in an array of properties (eg. returned by propfind) - * - * @param array &$props - * @param array|string $name property array or name - * @param string $ns =self::DEFAULT_PROP_NAMESPACE namespace, only if $prop is no array - * @return &array reference to property in $props or null if not found - */ - static function &find_prop(array &$props,$name,$ns=self::DEFAULT_PROP_NAMESPACE) - { - if (is_array($name)) - { - $ns = $name['ns']; - $name = $name['name']; - } - foreach($props as &$prop) - { - if ($prop['name'] == $name && $prop['ns'] == $ns) return $prop; - } - return null; - } - - /** - * stat working on just the eGW VFS (alias of url_stat) - * - * @param string $path filename with absolute path in the eGW VFS - * @param boolean $try_create_home =false should a non-existing home-directory be automatically created - * @return array - */ - static function stat($path,$try_create_home=false) - { - if ($path[0] != '/') - { - throw new egw_exception_assertion_failed("File '$path' is not an absolute path!"); - } - if (($stat = self::url_stat($path,0,$try_create_home))) - { - $stat = array_slice($stat,13); // remove numerical indices 0-12 - } - return $stat; - } - - /** - * lstat (not resolving symbolic links) working on just the eGW VFS (alias of url_stat) - * - * @param string $path filename with absolute path in the eGW VFS - * @param boolean $try_create_home =false should a non-existing home-directory be automatically created - * @return array - */ - static function lstat($path,$try_create_home=false) - { - if ($path[0] != '/') - { - throw new egw_exception_assertion_failed("File '$path' is not an absolute path!"); - } - if (($stat = self::url_stat($path,STREAM_URL_STAT_LINK,$try_create_home))) - { - $stat = array_slice($stat,13); // remove numerical indices 0-12 - } - return $stat; - } - - /** - * is_dir() version working only inside the vfs - * - * @param string $path - * @return boolean - */ - static function is_dir($path) - { - return $path[0] == '/' && is_dir(self::PREFIX.$path); - } - - /** - * is_link() version working only inside the vfs - * - * @param string $path - * @return boolean - */ - static function is_link($path) - { - return $path[0] == '/' && is_link(self::PREFIX.$path); - } - - /** - * file_exists() version working only inside the vfs - * - * @param string $path - * @return boolean - */ - static function file_exists($path) - { - return $path[0] == '/' && file_exists(self::PREFIX.$path); - } - - /** - * Mounts $url under $path in the vfs, called without parameter it returns the fstab - * - * The fstab is stored in the eGW configuration and used for all eGW users. - * - * @param string $url =null url of the filesystem to mount, eg. oldvfs://default/ - * @param string $path =null path to mount the filesystem in the vfs, eg. / - * @param boolean $check_url =null check if url is an existing directory, before mounting it - * default null only checks if url does not contain a $ as used in $user or $pass - * @param boolean $persitent_mount =true create a persitent mount, or only a temprary for current request - * @param boolean $clear_fstab =false true clear current fstab, false (default) only add given mount - * @return array|boolean array with fstab, if called without parameter or true on successful mount - */ - static function mount($url=null,$path=null,$check_url=null,$persitent_mount=true,$clear_fstab=false) - { - if (is_null($check_url)) $check_url = strpos($url,'$') === false; - - if (!isset($GLOBALS['egw_info']['server']['vfs_fstab'])) // happens eg. in setup - { - $api_config = config::read('phpgwapi'); - if (isset($api_config['vfs_fstab']) && is_array($api_config['vfs_fstab'])) - { - self::$fstab = $api_config['vfs_fstab']; - } - else - { - self::$fstab = array( - '/' => 'sqlfs://$host/', - '/apps' => 'links://$host/apps', - ); - } - unset($api_config); - } - if (is_null($url) || is_null($path)) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__.'('.array2string($url).','.array2string($path).') returns '.array2string(self::$fstab)); - return self::$fstab; - } - if (!self::$is_root) - { - if (self::LOG_LEVEL > 0) error_log(__METHOD__.'('.array2string($url).','.array2string($path).') permission denied, you are NOT root!'); - return false; // only root can mount - } - if ($clear_fstab) - { - self::$fstab = array(); - } - if (isset(self::$fstab[$path]) && self::$fstab[$path] === $url) - { - if (self::LOG_LEVEL > 0) error_log(__METHOD__.'('.array2string($url).','.array2string($path).') already mounted.'); - return true; // already mounted - } - self::load_wrapper(self::parse_url($url,PHP_URL_SCHEME)); - - if ($check_url && (!file_exists($url) || opendir($url) === false)) - { - if (self::LOG_LEVEL > 0) error_log(__METHOD__.'('.array2string($url).','.array2string($path).') url does NOT exist!'); - return false; // url does not exist - } - self::$fstab[$path] = $url; - - uksort(self::$fstab,create_function('$a,$b','return strlen($a)-strlen($b);')); - - if ($persitent_mount) - { - config::save_value('vfs_fstab',self::$fstab,'phpgwapi'); - $GLOBALS['egw_info']['server']['vfs_fstab'] = self::$fstab; - // invalidate session cache - if (method_exists($GLOBALS['egw'],'invalidate_session_cache')) // egw object in setup is limited - { - $GLOBALS['egw']->invalidate_session_cache(); - } - } - if (self::LOG_LEVEL > 1) error_log(__METHOD__.'('.array2string($url).','.array2string($path).') returns true (successful new mount).'); - return true; - } - - /** - * Unmounts a filesystem part of the vfs - * - * @param string $path url or path of the filesystem to unmount - */ - static function umount($path) - { - if (!self::$is_root) - { - if (self::LOG_LEVEL > 0) error_log(__METHOD__.'('.array2string($path).','.array2string($path).') permission denied, you are NOT root!'); - return false; // only root can mount - } - if (!isset(self::$fstab[$path]) && ($path = array_search($path,self::$fstab)) === false) - { - if (self::LOG_LEVEL > 0) error_log(__METHOD__.'('.array2string($path).') NOT mounted!'); - return false; // $path not mounted - } - unset(self::$fstab[$path]); - - config::save_value('vfs_fstab',self::$fstab,'phpgwapi'); - $GLOBALS['egw_info']['server']['vfs_fstab'] = self::$fstab; - // invalidate session cache - if (method_exists($GLOBALS['egw'],'invalidate_session_cache')) // egw object in setup is limited - { - $GLOBALS['egw']->invalidate_session_cache(); - } - if (self::LOG_LEVEL > 1) error_log(__METHOD__.'('.array2string($path).') returns true (successful unmount).'); - return true; - } - - /** - * Check if file is hidden: name starts with a '.' or is Thumbs.db - * - * @param string $path - * @return boolean - */ - public static function is_hidden($path) - { - $file = self::basename($path); - - return $file[0] == '.' || $file == 'Thumbs.db'; - } - - /** - * find = recursive search over the filesystem - * - * @param string|array $base base of the search - * @param array $options =null the following keys are allowed: - * - type => {d|f|F} d=dirs, f=files (incl. symlinks), F=files (incl. symlinks to files), default all - * - depth => {true|false(default)} put the contents of a dir before the dir itself - * - dirsontop => {true(default)|false} allways return dirs before the files (two distinct blocks) - * - mindepth,maxdepth minimal or maximal depth to be returned - * - name,path => pattern with *,? wildcards, eg. "*.php" - * - name_preg,path_preg => preg regular expresion, eg. "/(vfs|wrapper)/" - * - uid,user,gid,group,nouser,nogroup file belongs to user/group with given name or (numerical) id - * - mime => type[/subtype] or perl regular expression starting with a "/" eg. "/^(image|video)\\//i" - * - empty,size => (+|-|)N - * - cmin/mmin => (+|-|)N file/dir create/modified in the last N minutes - * - ctime/mtime => (+|-|)N file/dir created/modified in the last N days - * - url => false(default),true allow (and return) full URL's instead of VFS pathes (only set it, if you know what you doing securitywise!) - * - need_mime => false(default),true should we return the mime type - * - order => name order rows by name column - * - sort => (ASC|DESC) sort, default ASC - * - limit => N,[n=0] return N entries from position n on, which defaults to 0 - * - follow => {true|false(default)} follow symlinks - * - hidden => {true|false(default)} include hidden files (name starts with a '.' or is Thumbs.db) - * @param string|array/true $exec =null function to call with each found file/dir as first param and stat array as last param or - * true to return file => stat pairs - * @param array $exec_params =null further params for exec as array, path is always the first param and stat the last! - * @return array of pathes if no $exec, otherwise path => stat pairs - */ - static function find($base,$options=null,$exec=null,$exec_params=null) - { - //error_log(__METHOD__."(".print_r($base,true).",".print_r($options,true).",".print_r($exec,true).",".print_r($exec_params,true).")\n"); - - $type = $options['type']; // 'd', 'f' or 'F' - $dirs_last = $options['depth']; // put content of dirs before the dir itself - // show dirs on top by default, if no recursive listing (allways disabled if $type specified, as unnecessary) - $dirsontop = !$type && (isset($options['dirsontop']) ? (boolean)$options['dirsontop'] : isset($options['maxdepth'])&&$options['maxdepth']>0); - if ($dirsontop) $options['need_mime'] = true; // otherwise dirsontop can NOT work - - // process some of the options (need to be done only once) - if (isset($options['name']) && !isset($options['name_preg'])) // change from simple *,? wildcards to preg regular expression once - { - $options['name_preg'] = '/^'.str_replace(array('\\?','\\*'),array('.{1}','.*'),preg_quote($options['name'])).'$/i'; - } - if (isset($options['path']) && !isset($options['preg_path'])) // change from simple *,? wildcards to preg regular expression once - { - $options['path_preg'] = '/^'.str_replace(array('\\?','\\*'),array('.{1}','.*'),preg_quote($options['path'])).'$/i'; - } - if (!isset($options['uid'])) - { - if (isset($options['user'])) - { - $options['uid'] = $GLOBALS['egw']->accounts->name2id($options['user'],'account_lid','u'); - } - elseif (isset($options['nouser'])) - { - $options['uid'] = 0; - } - } - if (!isset($options['gid'])) - { - if (isset($options['group'])) - { - $options['gid'] = abs($GLOBALS['egw']->accounts->name2id($options['group'],'account_lid','g')); - } - elseif (isset($options['nogroup'])) - { - $options['gid'] = 0; - } - } - if ($options['order'] == 'mime') - { - $options['need_mime'] = true; // we need to return the mime colum - } - $url = $options['url']; - - if (!is_array($base)) - { - $base = array($base); - } - $result = array(); - foreach($base as $path) - { - if (!$url) - { - if ($path[0] != '/' || !egw_vfs::stat($path)) continue; - $path = egw_vfs::PREFIX . $path; - } - if (!isset($options['remove'])) - { - $options['remove'] = count($base) == 1 ? count(explode('/',$path))-3+(int)(substr($path,-1)!='/') : 0; - } - $is_dir = is_dir($path); - if ((int)$options['mindepth'] == 0 && (!$dirs_last || !$is_dir)) - { - self::_check_add($options,$path,$result); - } - if ($is_dir && (!isset($options['maxdepth']) || ($options['maxdepth'] > 0 && $options['depth'] < $options['maxdepth'])) && ($dir = @opendir($path))) - { - while(($fname = readdir($dir)) !== false) - { - if ($fname == '.' || $fname == '..') continue; // ignore current and parent dir! - - if (self::is_hidden($fname) && !$options['hidden']) continue; // ignore hidden files - - $file = self::concat($path, $fname); - - if ((int)$options['mindepth'] <= 1) - { - self::_check_add($options,$file,$result); - } - // only descend into subdirs, if it's a real dir (no link to a dir) or we should follow symlinks - if (is_dir($file) && ($options['follow'] || !is_link($file)) && (!isset($options['maxdepth']) || $options['maxdepth'] > 1)) - { - $opts = $options; - if ($opts['mindepth']) $opts['mindepth']--; - if ($opts['maxdepth']) $opts['depth']++; - unset($opts['order']); - unset($opts['limit']); - foreach(self::find($options['url']?$file:self::parse_url($file,PHP_URL_PATH),$opts,true) as $p => $s) - { - unset($result[$p]); - $result[$p] = $s; - } - } - } - closedir($dir); - } - if ($is_dir && (int)$options['mindepth'] == 0 && $dirs_last) - { - self::_check_add($options,$path,$result); - } - } - // sort code, to place directories before files, if $dirsontop enabled - $dirsfirst = $dirsontop ? '($a[mime]==\''.self::DIR_MIME_TYPE.'\')!==($b[mime]==\''.self::DIR_MIME_TYPE.'\')?'. - '($a[mime]==\''.self::DIR_MIME_TYPE.'\'?-1:1):' : ''; - // ordering of the rows - if (isset($options['order'])) - { - $sort = strtolower($options['sort']) == 'desc' ? '-' : ''; - switch($options['order']) - { - // sort numerical - case 'size': - case 'uid': - case 'gid': - case 'mode': - case 'ctime': - case 'mtime': - $code = $dirsfirst.$sort.'($a[\''.$options['order'].'\']-$b[\''.$options['order'].'\']);'; - // always use name as second sort criteria - $code = '$cmp = '.$code.' return $cmp ? $cmp : strcasecmp($a[\'name\'],$b[\'name\']);'; - $ok = uasort($result,create_function('$a,$b',$code)); - break; - - // sort alphanumerical - default: - $options['order'] = 'name'; - // fall throught - case 'name': - case 'mime': - $code = $dirsfirst.$sort.'strcasecmp($a[\''.$options['order'].'\'],$b[\''.$options['order'].'\']);'; - if ($options['order'] != 'name') - { - // always use name as second sort criteria - $code = '$cmp = '.$code.' return $cmp ? $cmp : strcasecmp($a[\'name\'],$b[\'name\']);'; - } - else - { - $code = 'return '.$code; - } - $ok = uasort($result,create_function('$a,$b',$code)); - break; - } - //echo "

order='$options[order]', sort='$options[sort]' --> uasort($result,create_function(,'$code'))=".array2string($ok)."

>\n"; - } - // limit resultset - self::$find_total = count($result); - if (isset($options['limit'])) - { - list($limit,$start) = explode(',',$options['limit']); - if (!$limit && !($limit = $GLOBALS['egw_info']['user']['preferences']['comman']['maxmatches'])) $limit = 15; - //echo "total=".egw_vfs::$find_total.", limit=$options[limit] --> start=$start, limit=$limit
\n"; - - if ((int)$start || self::$find_total > $limit) - { - $result = array_slice($result,(int)$start,(int)$limit,true); - } - } - //echo $path; _debug_array($result); - if ($exec !== true && is_callable($exec)) - { - if (!is_array($exec_params)) - { - $exec_params = is_null($exec_params) ? array() : array($exec_params); - } - foreach($result as $path => &$stat) - { - $options = $exec_params; - array_unshift($options,$path); - array_push($options,$stat); - //echo "calling ".print_r($exec,true).print_r($options,true)."\n"; - $stat = call_user_func_array($exec,$options); - } - return $result; - } - //error_log("egw_vfs::find($path)=".print_r(array_keys($result),true)); - if ($exec !== true) - { - return array_keys($result); - } - return $result; - } - - /** - * Function carying out the various (optional) checks, before files&dirs get returned as result of find - * - * @param array $options options, see egw_vfs::find(,$options) - * @param string $path name of path to add - * @param array &$result here we add the stat for the key $path, if the checks are successful - */ - private static function _check_add($options,$path,&$result) - { - $type = $options['type']; // 'd' or 'f' - - if ($options['url']) - { - $stat = @lstat($path); - } - else - { - $stat = self::url_stat($path,STREAM_URL_STAT_LINK); - } - if (!$stat) - { - return; // not found, should not happen - } - if ($type && (($type == 'd') == !($stat['mode'] & sqlfs_stream_wrapper::MODE_DIR) || // != is_dir() which can be true for symlinks - $type == 'F' && is_dir($path))) // symlink to a directory - { - return; // wrong type - } - $stat = array_slice($stat,13); // remove numerical indices 0-12 - $stat['path'] = self::parse_url($path,PHP_URL_PATH); - $stat['name'] = $options['remove'] > 0 ? implode('/',array_slice(explode('/',$stat['path']),$options['remove'])) : self::basename($path); - - if ($options['mime'] || $options['need_mime']) - { - $stat['mime'] = self::mime_content_type($path); - } - if (isset($options['name_preg']) && !preg_match($options['name_preg'],$stat['name']) || - isset($options['path_preg']) && !preg_match($options['path_preg'],$path)) - { - //echo "

!preg_match('{$options['name_preg']}','{$stat['name']}')

\n"; - return; // wrong name or path - } - if (isset($options['gid']) && $stat['gid'] != $options['gid'] || - isset($options['uid']) && $stat['uid'] != $options['uid']) - { - return; // wrong user or group - } - if (isset($options['mime']) && $options['mime'] != $stat['mime']) - { - if ($options['mime'][0] == '/') // perl regular expression given - { - if (!preg_match($options['mime'], $stat['mime'])) - { - return; // wrong mime-type - } - } - else - { - list($type,$subtype) = explode('/',$options['mime']); - // no subtype (eg. 'image') --> check only the main type - if ($subtype || substr($stat['mime'],0,strlen($type)+1) != $type.'/') - { - return; // wrong mime-type - } - } - } - if (isset($options['size']) && !self::_check_num($stat['size'],$options['size']) || - (isset($options['empty']) && !!$options['empty'] !== !$stat['size'])) - { - return; // wrong size - } - if (isset($options['cmin']) && !self::_check_num(round((time()-$stat['ctime'])/60),$options['cmin']) || - isset($options['mmin']) && !self::_check_num(round((time()-$stat['mtime'])/60),$options['mmin']) || - isset($options['ctime']) && !self::_check_num(round((time()-$stat['ctime'])/86400),$options['ctime']) || - isset($options['mtime']) && !self::_check_num(round((time()-$stat['mtime'])/86400),$options['mtime'])) - { - return; // not create/modified in the spezified time - } - // do we return url or just vfs pathes - if (!$options['url']) - { - $path = self::parse_url($path,PHP_URL_PATH); - } - $result[$path] = $stat; - } - - private static function _check_num($value,$argument) - { - if (is_int($argument) && $argument >= 0 || $argument[0] != '-' && $argument[0] != '+') - { - //echo "_check_num($value,$argument) check = == ".(int)($value == $argument)."\n"; - return $value == $argument; - } - if ($argument < 0) - { - //echo "_check_num($value,$argument) check < == ".(int)($value < abs($argument))."\n"; - return $value < abs($argument); - } - //echo "_check_num($value,$argument) check > == ".(int)($value > (int)substr($argument,1))."\n"; - return $value > (int) substr($argument,1); - } - - /** - * Recursiv remove all given url's, including it's content if they are files - * - * @param string|array $urls url or array of url's - * @param boolean $allow_urls =false allow to use url's, default no only pathes (to stay within the vfs) - * @throws egw_exception_assertion_failed when trainig to remove /, /apps or /home - * @return array - */ - static function remove($urls,$allow_urls=false) - { - //error_log(__METHOD__.'('.array2string($urls).')'); - // some precaution to never allow to (recursivly) remove /, /apps or /home - foreach((array)$urls as $url) - { - if (preg_match('/^\/?(home|apps|)\/*$/',self::parse_url($url,PHP_URL_PATH))) - { - throw new egw_exception_assertion_failed(__METHOD__.'('.array2string($urls).") Cautiously rejecting to remove folder '$url'!"); - } - } - return self::find($urls,array('depth'=>true,'url'=>$allow_urls,'hidden'=>true),array(__CLASS__,'_rm_rmdir')); - } - - /** - * Helper function for remove: either rmdir or unlink given url (depending if it's a dir or file) - * - * @param string $url - * @return boolean - */ - static function _rm_rmdir($url) - { - if ($url[0] == '/') - { - $url = self::PREFIX . $url; - } - if (is_dir($url) && !is_link($url)) - { - return egw_vfs::rmdir($url,0); - } - return egw_vfs::unlink($url); - } - - /** - * The stream_wrapper interface checks is_{readable|writable|executable} against the webservers uid, - * which is wrong in case of our vfs, as we use the current users id and memberships - * - * @param string $path - * @param int $check mode to check: one or more or'ed together of: 4 = egw_vfs::READABLE, - * 2 = egw_vfs::WRITABLE, 1 = egw_vfs::EXECUTABLE - * @return boolean - */ - static function is_readable($path,$check = self::READABLE) - { - return self::check_access($path,$check); - } - - /** - * The stream_wrapper interface checks is_{readable|writable|executable} against the webservers uid, - * which is wrong in case of our vfs, as we use the current users id and memberships - * - * @param string $path path - * @param int $check mode to check: one or more or'ed together of: 4 = egw_vfs::READABLE, - * 2 = egw_vfs::WRITABLE, 1 = egw_vfs::EXECUTABLE - * @param array|boolean $stat =null stat array or false, to not query it again - * @param int $user =null user used for check, if not current user (egw_vfs::$user) - * @return boolean - */ - static function check_access($path, $check, $stat=null, $user=null) - { - if (is_null($stat) && $user && $user != self::$user) - { - static $path_user_stat = array(); - - $backup_user = self::$user; - self::$user = $user; - - if (!isset($path_user_stat[$path]) || !isset($path_user_stat[$path][$user])) - { - self::clearstatcache($path); - - $path_user_stat[$path][$user] = self::url_stat($path, 0); - - self::clearstatcache($path); // we need to clear the stat-cache after the call too, as the next call might be the regular user again! - } - if (($stat = $path_user_stat[$path][$user])) - { - // some backend mounts use $user:$pass in their url, for them we have to deny access! - if (strpos(self::resolve_url($path, false, false, false), '$user') !== false) - { - $ret = false; - } - else - { - $ret = self::check_access($path, $check, $stat); - } - } - else - { - $ret = false; // no access, if we can not stat the file - } - self::$user = $backup_user; - - // we need to clear stat-cache again, after restoring original user, as eg. eACL is stored in session - self::clearstatcache($path); - - //error_log(__METHOD__."(path=$path||stat[name]={$stat['name']},stat[mode]=".sprintf('%o',$stat['mode']).",$check,$user) ".array2string($ret)); - return $ret; - } - - if (self::$is_root) - { - return true; - } - - // throw exception if stat array is used insead of path, can be removed soon - if (is_array($path)) - { - throw new egw_exception_wrong_parameter('path has to be string, use check_access($path,$check,$stat=null)!'); - } - // query stat array, if not given - if (is_null($stat)) - { - $stat = self::url_stat($path,0); - } - //error_log(__METHOD__."(path=$path||stat[name]={$stat['name']},stat[mode]=".sprintf('%o',$stat['mode']).",$check)"); - - if (!$stat) - { - //error_log(__METHOD__."(path=$path||stat[name]={$stat['name']},stat[mode]=".sprintf('%o',$stat['mode']).",$check) no stat array!"); - return false; // file not found - } - // check if we use an EGroupwre stream wrapper, or a stock php one - // if it's not an EGroupware one, we can NOT use uid, gid and mode! - if (($scheme = self::parse_url($stat['url'],PHP_URL_SCHEME)) && !(class_exists(self::scheme2class($scheme)))) - { - switch($check) - { - case self::READABLE: - return is_readable($stat['url']); - case self::WRITABLE: - return is_writable($stat['url']); - case self::EXECUTABLE: - return is_executable($stat['url']); - } - } - // check if other rights grant access - if (($stat['mode'] & $check) == $check) - { - //error_log(__METHOD__."(path=$path||stat[name]={$stat['name']},stat[mode]=".sprintf('%o',$stat['mode']).",$check) access via other rights!"); - return true; - } - // check if there's owner access and we are the owner - if (($stat['mode'] & ($check << 6)) == ($check << 6) && $stat['uid'] && $stat['uid'] == self::$user) - { - //error_log(__METHOD__."(path=$path||stat[name]={$stat['name']},stat[mode]=".sprintf('%o',$stat['mode']).",$check) access via owner rights!"); - return true; - } - // check if there's a group access and we have the right membership - if (($stat['mode'] & ($check << 3)) == ($check << 3) && $stat['gid']) - { - if (($memberships = $GLOBALS['egw']->accounts->memberships(self::$user, true)) && in_array(-abs($stat['gid']), $memberships)) - { - //error_log(__METHOD__."(path=$path||stat[name]={$stat['name']},stat[mode]=".sprintf('%o',$stat['mode']).",$check) access via group rights!"); - return true; - } - } - // check backend for extended acls (only if path given) - $ret = $path && self::_call_on_backend('check_extended_acl',array(isset($stat['url'])?$stat['url']:$path,$check),true); // true = fail silent if backend does not support - - //error_log(__METHOD__."(path=$path||stat[name]={$stat['name']},stat[mode]=".sprintf('%o',$stat['mode']).",$check) ".($ret ? 'backend extended acl granted access.' : 'no access!!!')); - return $ret; - } - - /** - * The stream_wrapper interface checks is_{readable|writable|executable} against the webservers uid, - * which is wrong in case of our vfs, as we use the current users id and memberships - * - * @param string $path - * @return boolean - */ - static function is_writable($path) - { - return self::is_readable($path,self::WRITABLE); - } - - /** - * The stream_wrapper interface checks is_{readable|writable|executable} against the webservers uid, - * which is wrong in case of our vfs, as we use the current users id and memberships - * - * @param string $path - * @return boolean - */ - static function is_executable($path) - { - return self::is_readable($path,self::EXECUTABLE); - } - - /** - * Check if path is a script and write access would be denied by backend - * - * @param string $path - * @return boolean true if $path is a script AND exec mount-option is NOT set, false otherwise - */ - static function deny_script($path) - { - return self::_call_on_backend('deny_script',array($path),true); - } - - /** - * Name of EACL array in session - */ - const SESSION_EACL = 'session-eacl'; - - /** - * Set or delete extended acl for a given path and owner (or delete them if is_null($rights) - * - * Does NOT check if user has the rights to set the extended acl for the given url/path! - * - * @param string $url string with path - * @param int $rights =null rights to set, or null to delete the entry - * @param int|boolean $owner =null owner for whom to set the rights, null for the current user, or false to delete all rights for $path - * @param boolean $session_only =false true: set eacl only for this session, does NO further checks currently! - * @return boolean true if acl is set/deleted, false on error - */ - static function eacl($url,$rights=null,$owner=null,$session_only=false) - { - if ($session_only) - { - $session_eacls =& egw_cache::getSession(__CLASS__, self::SESSION_EACL); - $session_eacls[] = array( - 'path' => $url[0] == '/' ? $url : egw_vfs::parse_url($url, PHP_URL_PATH), - 'owner' => $owner ? $owner : egw_vfs::$user, - 'rights' => $rights, - ); - return true; - } - return self::_call_on_backend('eacl',array($url,$rights,$owner)); - } - - /** - * Get all ext. ACL set for a path - * - * Calls itself recursive, to get the parent directories - * - * @param string $path - * @return array|boolean array with array('path'=>$path,'owner'=>$owner,'rights'=>$rights) or false if $path not found - */ - static function get_eacl($path) - { - $eacls = self::_call_on_backend('get_eacl',array($path),true); // true = fail silent (no PHP Warning) - - $session_eacls =& egw_cache::getSession(__CLASS__, self::SESSION_EACL); - if ($session_eacls) - { - // eacl is recursive, therefore we have to match all parent-dirs too - $paths = array($path); - while ($path && $path != '/') - { - $paths[] = $path = egw_vfs::dirname($path); - } - foreach((array)$session_eacls as $eacl) - { - if (in_array($eacl['path'], $paths)) - { - $eacls[] = $eacl; - } - } - - // sort by length descending, to show precedence - usort($eacls, function($a, $b) { - return strlen($b['path']) - strlen($a['path']); - }); - } - return $eacls; - } - - /** - * Store properties for a single ressource (file or dir) - * - * @param string $path string with path - * @param array $props array of array with values for keys 'name', 'ns', 'val' (null to delete the prop) - * @return boolean true if props are updated, false otherwise (eg. ressource not found) - */ - static function proppatch($path,array $props) - { - return self::_call_on_backend('proppatch',array($path,$props)); - } - - /** - * Default namespace for properties set by eGroupware: comment or custom fields (leading #) - * - */ - const DEFAULT_PROP_NAMESPACE = 'http://egroupware.org/'; - - /** - * Read properties for a ressource (file, dir or all files of a dir) - * - * @param array|string $path (array of) string with path - * @param string $ns ='http://egroupware.org/' namespace if propfind should be limited to a single one, otherwise use null - * @return array|boolean array with props (values for keys 'name', 'ns', 'val'), or path => array of props for is_array($path) - * false if $path does not exist - */ - static function propfind($path,$ns=self::DEFAULT_PROP_NAMESPACE) - { - return self::_call_on_backend('propfind',array($path,$ns),true); // true = fail silent (no PHP Warning) - } - - /** - * Private constructor to prevent instanciating this class, only it's static methods should be used - */ - private function __construct() - { - - } - - /** - * Convert a symbolic mode string or octal mode to an integer - * - * @param string|int $set comma separated mode string to set [ugo]+[+=-]+[rwx]+ - * @param int $mode =0 current mode of the file, necessary for +/- operation - * @return int - */ - static function mode2int($set,$mode=0) - { - if (is_int($set)) // already an integer - { - return $set; - } - if (is_numeric($set)) // octal string - { - //error_log(__METHOD__."($set,$mode) returning ".(int)base_convert($set,8,10)); - return (int)base_convert($set,8,10); // convert octal to decimal - } - foreach(explode(',',$set) as $s) - { - $matches = null; - if (!preg_match($use='/^([ugoa]*)([+=-]+)([rwx]+)$/',$s,$matches)) - { - $use = str_replace(array('/','^','$','(',')'),'',$use); - throw new egw_exception_wrong_userinput("$s is not an allowed mode, use $use !"); - } - $base = (strpos($matches[3],'r') !== false ? self::READABLE : 0) | - (strpos($matches[3],'w') !== false ? self::WRITABLE : 0) | - (strpos($matches[3],'x') !== false ? self::EXECUTABLE : 0); - - for($n = $m = 0; $n < strlen($matches[1]); $n++) - { - switch($matches[1][$n]) - { - case 'o': - $m |= $base; - break; - case 'g': - $m |= $base << 3; - break; - case 'u': - $m |= $base << 6; - break; - default: - case 'a': - $m = $base | ($base << 3) | ($base << 6); - } - } - switch($matches[2]) - { - case '+': - $mode |= $m; - break; - case '=': - $mode = $m; - break; - case '-': - $mode &= ~$m; - } - } - //error_log(__METHOD__."($set,) returning ".sprintf('%o',$mode)); - return $mode; - } - - /** - * Convert a numerical mode to a symbolic mode-string - * - * @param int $mode - * @return string - */ - static function int2mode( $mode ) - { - if(($mode & self::MODE_LINK) == self::MODE_LINK) // Symbolic Link - { - $sP = 'l'; - } - elseif(($mode & 0xC000) == 0xC000) // Socket - { - $sP = 's'; - } - elseif($mode & 0x1000) // FIFO pipe - { - $sP = 'p'; - } - elseif($mode & 0x2000) // Character special - { - $sP = 'c'; - } - elseif($mode & 0x4000) // Directory - { - $sP = 'd'; - } - elseif($mode & 0x6000) // Block special - { - $sP = 'b'; - } - elseif($mode & 0x8000) // Regular - { - $sP = '-'; - } - else // UNKNOWN - { - $sP = 'u'; - } - - // owner - $sP .= (($mode & 0x0100) ? 'r' : '-') . - (($mode & 0x0080) ? 'w' : '-') . - (($mode & 0x0040) ? (($mode & 0x0800) ? 's' : 'x' ) : - (($mode & 0x0800) ? 'S' : '-')); - - // group - $sP .= (($mode & 0x0020) ? 'r' : '-') . - (($mode & 0x0010) ? 'w' : '-') . - (($mode & 0x0008) ? (($mode & 0x0400) ? 's' : 'x' ) : - (($mode & 0x0400) ? 'S' : '-')); - - // world - $sP .= (($mode & 0x0004) ? 'r' : '-') . - (($mode & 0x0002) ? 'w' : '-') . - (($mode & 0x0001) ? (($mode & 0x0200) ? 't' : 'x' ) : - (($mode & 0x0200) ? 'T' : '-')); - - return $sP; - } - - /** - * Get the closest mime icon - * - * @param string $mime_type - * @param boolean $et_image =true return $app/$icon string for etemplate (default) or html img tag if false - * @param int $size =128 - * @return string - */ - static function mime_icon($mime_type, $et_image=true, $size=128) - { - if ($mime_type == egw_vfs::DIR_MIME_TYPE) - { - $mime_type = 'Directory'; - } - if(!$mime_type) - { - $mime_type = 'unknown'; - } - $mime_full = strtolower(str_replace ('/','_',$mime_type)); - list($mime_part) = explode('_',$mime_full); - - if (!($img=common::image('etemplate',$icon='mime'.$size.'_'.$mime_full)) && - // check mime-alias-map before falling back to more generic icons - !(isset(mime_magic::$mime_alias_map[$mime_type]) && - ($img=common::image('etemplate',$icon='mime'.$size.'_'.str_replace('/','_',mime_magic::$mime_alias_map[$mime_full])))) && - !($img=common::image('etemplate',$icon='mime'.$size.'_'.$mime_part))) - { - $img = common::image('etemplate',$icon='mime'.$size.'_unknown'); - } - if ($et_image === 'url') - { - return $img; - } - if ($et_image) - { - return 'etemplate/'.$icon; - } - return html::image('etemplate',$icon,mime_magic::mime2label($mime_type)); - } - - /** - * Human readable size values in k, M or G - * - * @param int $size - * @return string - */ - static function hsize($size) - { - if ($size < 1024) return $size; - if ($size < 1024*1024) return sprintf('%3.1lfk',(float)$size/1024); - if ($size < 1024*1024*1024) return sprintf('%3.1lfM',(float)$size/(1024*1024)); - return sprintf('%3.1lfG',(float)$size/(1024*1024*1024)); - } - - /** - * Size in bytes, from human readable - * - * From PHP ini_get docs, Ivo Mandalski 15-Nov-2011 08:27 - */ - static function int_size($_val) - { - if(empty($_val))return 0; - - $val = trim($_val); - - $matches = null; - preg_match('#([0-9]+)[\s]*([a-z]+)#i', $val, $matches); - - $last = ''; - if(isset($matches[2])){ - $last = $matches[2]; - } - - if(isset($matches[1])){ - $val = (int) $matches[1]; - } - - switch (strtolower($last)) - { - case 'g': - case 'gb': - $val *= 1024; - case 'm': - case 'mb': - $val *= 1024; - case 'k': - case 'kb': - $val *= 1024; - } - - return (int) $val; - } - - /** - * like basename($path), but also working if the 1. char of the basename is non-ascii - * - * @param string $_path - * @return string - */ - static function basename($_path) - { - list($path) = explode('?',$_path); // remove query - $parts = explode('/',$path); - - return array_pop($parts); - } - - /** - * Get the directory / parent of a given path or url(!), return false for '/'! - * - * Also works around PHP under Windows returning dirname('/something') === '\\', which is NOT understood by EGroupware's VFS! - * - * @param string $_url path or url - * @return string|boolean parent or false if there's none ($path == '/') - */ - static function dirname($_url) - { - list($url,$query) = explode('?',$_url,2); // strip the query first, as it can contain slashes - - if ($url == '/' || $url[0] != '/' && self::parse_url($url,PHP_URL_PATH) == '/') - { - //error_log(__METHOD__."($url) returning FALSE: already in root!"); - return false; - } - $parts = explode('/',$url); - if (substr($url,-1) == '/') array_pop($parts); - array_pop($parts); - if ($url[0] != '/' && count($parts) == 3 || count($parts) == 1 && $parts[0] === '') - { - array_push($parts,''); // scheme://host is wrong (no path), has to be scheme://host/ - } - //error_log(__METHOD__."($url)=".implode('/',$parts).($query ? '?'.$query : '')); - return implode('/',$parts).($query ? '?'.$query : ''); - } - - /** - * Check if the current use has owner rights for the given path or stat - * - * We define all eGW admins the owner of the group directories! - * - * @param string $path - * @param array $stat =null stat for path, default queried by this function - * @return boolean - */ - static function has_owner_rights($path,array $stat=null) - { - if (!$stat) $stat = self::url_stat($path,0); - - return $stat['uid'] == self::$user || // user is the owner - self::$is_root || // class runs with root rights - !$stat['uid'] && $stat['gid'] && self::$is_admin; // group directory and user is an eGW admin - } - - /** - * Concat a relative path to an url, taking into account, that the url might already end with a slash or the path starts with one or is empty - * - * Also normalizing the path, as the relative path can contain ../ - * - * @param string $_url base url or path, might end in a / - * @param string $relative relative path to add to $url - * @return string - */ - static function concat($_url,$relative) - { - list($url,$query) = explode('?',$_url,2); - if (substr($url,-1) == '/') $url = substr($url,0,-1); - $ret = ($relative === '' || $relative[0] == '/' ? $url.$relative : $url.'/'.$relative); - - // now normalize the path (remove "/something/..") - while (strpos($ret,'/../') !== false) - { - list($a_str,$b_str) = explode('/../',$ret,2); - $a = explode('/',$a_str); - array_pop($a); - $b = explode('/',$b_str); - $ret = implode('/',array_merge($a,$b)); - } - return $ret.($query ? (strpos($url,'?')===false ? '?' : '&').$query : ''); - } - - /** - * Build an url from it's components (reverse of parse_url) - * - * @param array $url_parts values for keys 'scheme', 'host', 'user', 'pass', 'query', 'fragment' (all but 'path' are optional) - * @return string - */ - static function build_url(array $url_parts) - { - $url = (!isset($url_parts['scheme'])?'':$url_parts['scheme'].'://'. - (!isset($url_parts['user'])?'':$url_parts['user'].(!isset($url_parts['pass'])?'':':'.$url_parts['pass']).'@'). - $url_parts['host']).$url_parts['path']. - (!isset($url_parts['query'])?'':'?'.$url_parts['query']). - (!isset($url_parts['fragment'])?'':'?'.$url_parts['fragment']); - //error_log(__METHOD__.'('.array2string($url_parts).") = '".$url."'"); - return $url; - } - - /** - * URL to download a file - * - * We use our webdav handler as download url instead of an own download method. - * The webdav hander (filemanager/webdav.php) recognices eGW's session cookie and of cause understands regular GET requests. - * - * Please note: If you dont use eTemplate or the html class, you have to run this url throught egw::link() to get a full url - * - * @param string $path - * @param boolean $force_download =false add header('Content-disposition: filename="' . basename($path) . '"'), currently not supported! - * @todo get $force_download working through webdav - * @return string - */ - static function download_url($path,$force_download=false) - { - if (($url = self::_call_on_backend('download_url',array($path,$force_download),true))) - { - return $url; - } - if ($path[0] != '/') - { - $path = self::parse_url($path,PHP_URL_PATH); - } - // we do NOT need to encode % itself, as our path are already url encoded, with the exception of ' ' and '+' - // we urlencode double quotes '"', as that fixes many problems in html markup - return '/webdav.php'.strtr($path,array('+' => '%2B',' ' => '%20','"' => '%22')).($force_download ? '?download' : ''); - } - - /** - * Download the given file list as a ZIP - * - * @param array $_files List of files to include in the zip - * @param string $name optional Zip file name. If not provided, it will be determined automatically from the files - * - * @return undefined - */ - public static function download_zip(Array $_files, $name = false) - { - error_log(__METHOD__ . ': '.implode(',',$_files)); - - // Create zip file - $zip_file = tempnam($GLOBALS['egw_info']['server']['temp_dir'], 'zip'); - - $zip = new ZipArchive(); - if (!$zip->open($zip_file, ZipArchive::OVERWRITE)) - { - throw new egw_exception("Cannot open zip file for writing."); - } - - // Find lowest common directory, to use relative paths - // eg: User selected /home/nathan/picture.jpg, /home/Pictures/logo.jpg - // We want /home - $dirs = array(); - foreach($_files as $file) - { - $dirs[] = self::dirname($file); - } - $paths = array_unique($dirs); - if(count($paths) > 0) - { - // Shortest to longest - usort($paths, function($a, $b) { - return strlen($a) - strlen($b); - }); - - // Start with shortest, pop off sub-directories that don't match - $parts = explode('/',$paths[0]); - foreach($paths as $path) - { - $dirs = explode('/',$path); - foreach($dirs as $dir_index => $dir) - { - if($parts[$dir_index] && $parts[$dir_index] != $dir) - { - unset($parts[$dir_index]); - } - } - } - $base_dir = implode('/', $parts); - } - else - { - $base_dir = $paths[0]; - } - - // Remove 'unsafe' filename characters - // (en.wikipedia.org/wiki/Filename#Reserved_characters_and_words) - $replace = array( - // Linux - '/', - // Windows - '\\','?','%','*',':','|',/*'.',*/ '"','<','>' - ); - - // A nice name for the user, - $filename = $GLOBALS['egw_info']['server']['site_title'] . '_' . - str_replace($replace,'_',( - $name ? $name : ( - count($_files) == 1 ? - // Just one file (hopefully a directory?) selected - self::basename($_files[0]) : - // Use the lowest common directory (eg: Infolog, Open, nathan) - self::basename($base_dir)) - )) . '.zip'; - - // Make sure basename is a dir - if(substr($base_dir, -1) != '/') - { - $base_dir .='/'; - } - - // Go into directories, find them all - $files = self::find($_files); - $links = array(); - - // We need to remove them _after_ we're done - $tempfiles = array(); - - // Give 1 second per file - set_time_limit(count($files)); - - // Add files to archive - foreach($files as &$addfile) - { - // Use relative paths inside zip - $relative = substr($addfile, strlen($base_dir)); - - // Use safe names - replace unsafe chars, convert to ASCII (ZIP spec says CP437, but we'll try) - $path = explode('/',$relative); - $_name = translation::convert(translation::to_ascii(implode('/', str_replace($replace,'_',$path))),false,'ASCII'); - - // Don't go infinite with app entries - if(self::is_link($addfile)) - { - if(in_array($addfile, $links)) continue; - $links[] = $addfile; - } - // Add directory - if empty, client app might not show it though - if(self::is_dir($addfile)) - { - // Zip directories - $zip->addEmptyDir($addfile); - } - else if(self::is_readable($addfile)) - { - // Copy to temp file, as ZipArchive fails to read VFS - $temp = tempnam($GLOBALS['egw_info']['server']['temp_dir'], 'zip_'); - $from = egw_vfs::fopen($addfile,'r'); - $to = fopen($temp,'w'); - if(!stream_copy_to_stream($from,$to) || !$zip->addFile($temp, $_name)) - { - unlink($temp); - trigger_error("Could not add $addfile to ZIP file", E_USER_ERROR); - continue; - } - // Keep temp file until _after_ zipping is done - $tempfiles[] = $temp; - - // Add comment in - $props = self::propfind($addfile); - if($props) - { - $comment = self::find_prop($props,'comment'); - if($comment) - { - $zip->setCommentName($_name, $comment); - } - } - unset($props); - } - } - - // Set a comment to help tell them apart - $zip->setArchiveComment(lang('Created by %1', $GLOBALS['egw_info']['user']['account_lid']) . ' ' .egw_time::to()); - - // Record total for debug, not available after close() - $total_files = $zip->numFiles; - - $result = $zip->close(); - if(!$result || !filesize($zip_file)) - { - error_log('close() result: '.array2string($result)); - return 'Error creating zip file'; - } - - error_log("Total files: " . $total_files . " Peak memory to zip: " . self::hsize(memory_get_peak_usage(true))); - - // Stop any buffering - while(ob_get_level() > 0) - { - ob_end_clean(); - } - - // Stream the file to the client - header("Content-Type: application/zip"); - header("Content-Length: " . filesize($zip_file)); - header("Content-Disposition: attachment; filename=\"$filename\""); - readfile($zip_file); - - unlink($zip_file); - foreach($tempfiles as $temp_file) - { - unlink($temp_file); - } - - // Make sure to exit after, if you don't want to add to the ZIP - } - - /** - * We cache locks within a request, as HTTP_WebDAV_Server generates so many, that it can be a bottleneck - * - * @var array - */ - static protected $lock_cache; - - /** - * Log (to error log) all calls to lock(), unlock() or checkLock() - * - */ - const LOCK_DEBUG = false; - - /** - * lock a ressource/path - * - * @param string $path path or url - * @param string &$token - * @param int &$timeout - * @param string &$owner - * @param string &$scope - * @param string &$type - * @param boolean $update =false - * @param boolean $check_writable =true should we check if the ressource is writable, before granting locks, default yes - * @return boolean true on success - */ - static function lock($path,&$token,&$timeout,&$owner,&$scope,&$type,$update=false,$check_writable=true) - { - // we require write rights to lock/unlock a resource - if (!$path || $update && !$token || $check_writable && - !(egw_vfs::is_writable($path) || !egw_vfs::file_exists($path) && egw_vfs::is_writable(egw_vfs::dirname($path)))) - { - return false; - } - // remove the lock info evtl. set in the cache - unset(self::$lock_cache[$path]); - - if ($timeout < 1000000) // < 1000000 is a relative timestamp, so we add the current time - { - $timeout += time(); - } - - if ($update) // Lock Update - { - if (($ret = (boolean)($row = self::$db->select(self::LOCK_TABLE,array('lock_owner','lock_exclusive','lock_write'),array( - 'lock_path' => $path, - 'lock_token' => $token, - ),__LINE__,__FILE__)->fetch()))) - { - $owner = $row['lock_owner']; - $scope = egw_db::from_bool($row['lock_exclusive']) ? 'exclusive' : 'shared'; - $type = egw_db::from_bool($row['lock_write']) ? 'write' : 'read'; - - self::$db->update(self::LOCK_TABLE,array( - 'lock_expires' => $timeout, - 'lock_modified' => time(), - ),array( - 'lock_path' => $path, - 'lock_token' => $token, - ),__LINE__,__FILE__); - } - } - // HTTP_WebDAV_Server does this check before calling LOCK, but we want to be complete and usable outside WebDAV - elseif(($lock = self::checkLock($path)) && ($lock['scope'] == 'exclusive' || $scope == 'exclusive')) - { - $ret = false; // there's alread a lock - } - else - { - // HTTP_WebDAV_Server sets owner and token, but we want to be complete and usable outside WebDAV - if (!$owner || $owner == 'unknown') - { - $owner = 'mailto:'.$GLOBALS['egw_info']['user']['account_email']; - } - if (!$token) - { - if (strpos(ini_get('include_path'), EGW_API_INC) === false) - { - ini_set('include_path', EGW_API_INC.PATH_SEPARATOR.ini_get('include_path')); - } - require_once('HTTP/WebDAV/Server.php'); - $token = HTTP_WebDAV_Server::_new_locktoken(); - } - try { - self::$db->insert(self::LOCK_TABLE,array( - 'lock_token' => $token, - 'lock_path' => $path, - 'lock_created' => time(), - 'lock_modified' => time(), - 'lock_owner' => $owner, - 'lock_expires' => $timeout, - 'lock_exclusive' => $scope == 'exclusive', - 'lock_write' => $type == 'write', - ),false,__LINE__,__FILE__); - $ret = true; - } - catch(egw_exception_db $e) { - unset($e); - $ret = false; // there's already a lock - } - } - if (self::LOCK_DEBUG) error_log(__METHOD__."($path,$token,$timeout,$owner,$scope,$type,update=$update,check_writable=$check_writable) returns ".($ret ? 'true' : 'false')); - return $ret; - } - - /** - * unlock a ressource/path - * - * @param string $path path to unlock - * @param string $token locktoken - * @param boolean $check_writable =true should we check if the ressource is writable, before granting locks, default yes - * @return boolean true on success - */ - static function unlock($path,$token,$check_writable=true) - { - // we require write rights to lock/unlock a resource - if ($check_writable && !egw_vfs::is_writable($path)) - { - return false; - } - if (($ret = self::$db->delete(self::LOCK_TABLE,array( - 'lock_path' => $path, - 'lock_token' => $token, - ),__LINE__,__FILE__) && self::$db->affected_rows())) - { - // remove the lock from the cache too - unset(self::$lock_cache[$path]); - } - if (self::LOCK_DEBUG) error_log(__METHOD__."($path,$token,$check_writable) returns ".($ret ? 'true' : 'false')); - return $ret; - } - - /** - * checkLock() helper - * - * @param string resource path to check for locks - * @return array|boolean false if there's no lock, else array with lock info - */ - static function checkLock($path) - { - if (isset(self::$lock_cache[$path])) - { - if (self::LOCK_DEBUG) error_log(__METHOD__."($path) returns from CACHE ".str_replace(array("\n",' '),'',print_r(self::$lock_cache[$path],true))); - return self::$lock_cache[$path]; - } - $where = 'lock_path='.self::$db->quote($path); - // ToDo: additional check parent dirs for locks and children of the requested directory - //$where .= ' OR '.self::$db->quote($path).' LIKE '.self::$db->concat('lock_path',"'%'").' OR lock_path LIKE '.self::$db->quote($path.'%'); - // ToDo: shared locks can return multiple rows - if (($result = self::$db->select(self::LOCK_TABLE,'*',$where,__LINE__,__FILE__)->fetch())) - { - $result = egw_db::strip_array_keys($result,'lock_'); - $result['type'] = egw_db::from_bool($result['write']) ? 'write' : 'read'; - $result['scope'] = egw_db::from_bool($result['exclusive']) ? 'exclusive' : 'shared'; - $result['depth'] = egw_db::from_bool($result['recursive']) ? 'infinite' : 0; - } - if ($result && $result['expires'] < time()) // lock is expired --> remove it - { - self::$db->delete(self::LOCK_TABLE,array( - 'lock_path' => $result['path'], - 'lock_token' => $result['token'], - ),__LINE__,__FILE__); - - if (self::LOCK_DEBUG) error_log(__METHOD__."($path) lock is expired at ".date('Y-m-d H:i:s',$result['expires'])." --> removed"); - $result = false; - } - if (self::LOCK_DEBUG) error_log(__METHOD__."($path) returns ".($result?array2string($result):'false')); - return self::$lock_cache[$path] = $result; - } - - /** - * Get backend specific information (data and etemplate), to integrate as tab in filemanagers settings dialog - * - * @param string $path - * @param array $content =null - * @return array|boolean array with values for keys 'data','etemplate','name','label','help' or false if not supported by backend - */ - static function getExtraInfo($path,array $content=null) - { - $extra = array(); - if (($extra_info = self::_call_on_backend('extra_info',array($path,$content),true))) // true = fail silent if backend does NOT support it - { - $extra[] = $extra_info; - } - - if (($vfs_extra = $GLOBALS['egw']->hooks->process(array( - 'location' => 'vfs_extra', - 'path' => $path, - 'content' => $content, - )))) - { - foreach($vfs_extra as $data) - { - $extra = $extra ? array_merge($extra, $data) : $data; - } - } - return $extra; - } - - /** - * Mapps entries of applications to a path for the locking - * - * @param string $app - * @param int|string $id - * @return string - */ - static function app_entry_lock_path($app,$id) - { - return "/apps/$app/entry/$id"; - } - - /** - * Encoding of various special characters, which can NOT be unencoded in file-names, as they have special meanings in URL's - * - * @var array - */ - static public $encode = array( - //'%' => '%25', // % should be encoded, but easily leads to double encoding, therefore better NOT encodig it - '#' => '%23', - '?' => '%3F', - '/' => '', // better remove it completly - ); - - /** - * Encode a path component: replacing certain chars with their urlencoded counterparts - * - * Not all chars get encoded, slashes '/' are silently removed! - * - * To reverse the encoding, eg. to display a filename to the user, you have to use egw_vfs::decodePath() - * - * @param string|array $component - * @return string|array - */ - static public function encodePathComponent($component) - { - return str_replace(array_keys(self::$encode),array_values(self::$encode),$component); - } - - /** - * Encode a path: replacing certain chars with their urlencoded counterparts - * - * To reverse the encoding, eg. to display a filename to the user, you have to use egw_vfs::decodePath() - * - * @param string $path - * @return string - */ - static public function encodePath($path) - { - return implode('/',self::encodePathComponent(explode('/',$path))); - } - - /** - * Decode a path: rawurldecode(): mostly urldecode(), but do NOT decode '+', as we're NOT encoding it! - * - * Used eg. to translate a path for displaying to the User. - * - * @param string $path - * @return string - */ - static public function decodePath($path) - { - return rawurldecode($path); - } - - /** - * Initialise our static vars - */ - static function init_static() - { - // if special user/vfs_user given (eg. from sharing) use it instead default user/account_id - self::$user = (int)(isset($GLOBALS['egw_info']['user']['vfs_user']) ? - $GLOBALS['egw_info']['user']['vfs_user'] : $GLOBALS['egw_info']['user']['account_id']); - self::$is_admin = isset($GLOBALS['egw_info']['user']['apps']['admin']); - self::$db = isset($GLOBALS['egw_setup']->db) ? $GLOBALS['egw_setup']->db : $GLOBALS['egw']->db; - self::$lock_cache = array(); - } - - /** - * Returns the URL to the thumbnail of the given file. The thumbnail may simply - * be the mime-type icon, or - if activated - the preview with the given thsize. - * - * @param string $file name of the file - * @param int $thsize the size of the preview - false if the default should be used. - * @param string $mime if you already know the mime type of the file, you can supply - * it here. Otherwise supply "false". - */ - public static function thumbnail_url($file, $thsize = false, $mime = false) - { - // Retrive the mime-type of the file - if (!$mime) - { - $mime = egw_vfs::mime_content_type($file); - } - - $image = ""; - - // Seperate the mime type into the primary and the secondary part - list($mime_main, $mime_sub) = explode('/', $mime); - - if ($mime_main == 'egw') - { - $image = common::image($mime_sub, 'navbar'); - } - else if ($file && $mime_main == 'image' && in_array($mime_sub, array('png','jpeg','jpg','gif','bmp')) && - (string)$GLOBALS['egw_info']['server']['link_list_thumbnail'] != '0' && - (string)$GLOBALS['egw_info']['user']['preferences']['common']['link_list_thumbnail'] != '0' && - ($stat = egw_vfs::stat($file)) && $stat['size'] < 1500000) - { - if (substr($file, 0, 6) == '/apps/') - { - $file = self::parse_url(egw_vfs::resolve_url_symlinks($file), PHP_URL_PATH); - } - - //Assemble the thumbnail parameters - $thparams = array(); - $thparams['path'] = $file; - if ($thsize) - { - $thparams['thsize'] = $thsize; - } - $image = $GLOBALS['egw']->link('/etemplate/thumbnail.php', $thparams); - } - else - { - list($app, $name) = explode("/", egw_vfs::mime_icon($mime), 2); - $image = common::image($app, $name); - } - - return $image; - } - - /** - * Get the configured start directory for the current user - * - * @return string - */ - static public function get_home_dir() - { - $start = '/home/'.$GLOBALS['egw_info']['user']['account_lid']; - - // check if user specified a valid startpath in his prefs --> use it - if (($path = $GLOBALS['egw_info']['user']['preferences']['filemanager']['startfolder']) && - $path[0] == '/' && egw_vfs::is_dir($path) && egw_vfs::check_access($path, egw_vfs::READABLE)) - { - $start = $path; - } - return $start; - } - - /** - * Copies the files given in $src to $dst. - * - * @param array $src contains the source file - * @param string $dst is the destination directory - */ - static public function copy_files(array $src, $dst, &$errs, array &$copied) - { - if (self::is_dir($dst)) - { - foreach ($src as $file) - { - // Check whether the file has already been copied - prevents from - // recursion - if (!in_array($file, $copied)) - { - // Calculate the target filename - $target = egw_vfs::concat($dst, egw_vfs::basename($file)); - - if (self::is_dir($file)) - { - if ($file !== $target) - { - // Create the target directory - egw_vfs::mkdir($target,null,STREAM_MKDIR_RECURSIVE); - - $copied[] = $file; - $copied[] = $target; // < newly created folder must not be copied again! - if (egw_vfs::copy_files(egw_vfs::find($file), $target, - $errs, $copied)) - { - continue; - } - } - - $errs++; - } - else - { - // Copy a single file - check whether the file should be - // copied onto itself. - // TODO: Check whether target file already exists and give - // return those files so that a dialog might be displayed - // on the client side which lets the user decide. - if ($target !== $file && egw_vfs::copy($file, $target)) - { - $copied[] = $file; - } - else - { - $errs++; - } - } - } - } - } - - return $errs == 0; - } - - /** - * Moves the files given in src to dst - */ - static public function move_files(array $src, $dst, &$errs, array &$moved) - { - if (egw_vfs::is_dir($dst)) - { - foreach($src as $file) - { - $target = egw_vfs::concat($dst, egw_vfs::basename($file)); - - if ($file != $target && egw_vfs::rename($file, $target)) - { - $moved[] = $file; - } - else - { - ++$errs; - } - } - - return $errs == 0; - } - - return false; - } - - /** - * Copy an uploaded file into the vfs, optionally set some properties (eg. comment or other cf's) - * - * Treat copying incl. properties as atomar operation in respect of notifications (one notification about an added file). - * - * @param array|string $src path to uploaded file or etemplate file array (value for key 'tmp_name') - * @param string $target path or directory to copy uploaded file - * @param array|string $props =null array with properties (name => value pairs, eg. 'comment' => 'FooBar','#cfname' => 'something'), - * array as for proppatch (array of array with values for keys 'name', 'val' and optional 'ns') or string with comment - * @param boolean $check_is_uploaded_file =true should method perform an is_uploaded_file check, default yes - * @return boolean|array stat array on success, false on error - */ - static public function copy_uploaded($src,$target,$props=null,$check_is_uploaded_file=true) - { - $tmp_name = is_array($src) ? $src['tmp_name'] : $src; - - if (self::stat($target) && self::is_dir($target)) - { - $target = self::concat($target, self::encodePathComponent(is_array($src) ? $src['name'] : basename($tmp_name))); - } - if ($check_is_uploaded_file && !is_uploaded_file($tmp_name)) - { - if (self::LOG_LEVEL) error_log(__METHOD__."($tmp_name, $target, ".array2string($props).",$check_is_uploaded_file) returning FALSE !is_uploaded_file()"); - return false; - } - if (!(self::is_writable($target) || self::is_writable(self::dirname($target)))) - { - if (self::LOG_LEVEL) error_log(__METHOD__."($tmp_name, $target, ".array2string($props).",$check_is_uploaded_file) returning FALSE !writable"); - return false; - } - if ($props) - { - if (!is_array($props)) $props = array(array('name' => 'comment','val' => $props)); - - // if $props is name => value pairs, convert it to internal array or array with values for keys 'name', 'val' and optional 'ns' - if (!isset($props[0])) - { - foreach($props as $name => $val) - { - if (($name == 'comment' || $name[0] == '#') && $val) // only copy 'comment' and cfs - { - $vfs_props[] = array( - 'name' => $name, - 'val' => $val, - ); - } - } - $props = $vfs_props; - } - } - if ($props) - { - // set props before copying the file, so notifications already contain them - if (!self::stat($target)) - { - self::touch($target); // create empty file, to be able to attach properties - self::$treat_as_new = true; // notify as new - } - self::proppatch($target, $props); - } - $ret = copy($tmp_name,self::PREFIX.$target) ? self::stat($target) : false; - if (self::LOG_LEVEL > 1 || !$ret && self::LOG_LEVEL) error_log(__METHOD__."($tmp_name, $target, ".array2string($props).") returning ".array2string($ret)); - return $ret; - } - - /** - * Compare two files from vfs or local file-system for identical content - * - * VFS files must use URL, to be able to distinguish them eg. from temp. files! - * - * @param string $file1 vfs-url or local path, eg. /tmp/some-file.txt or vfs://default/home/user/some-file.txt - * @param string $file2 -- " -- - * @return boolean true: if files are identical, false: if not or file not found - */ - public static function compare($file1, $file2) - { - if (filesize($file1) != filesize($file2) || - !($fp1 = fopen($file1, 'r')) || !($fp2 = fopen($file2, 'r'))) - { - //error_log(__METHOD__."($file1, $file2) returning FALSE (different size)"); - return false; - } - while (($read1 = fread($fp1, 8192)) !== false && - ($read2 = fread($fp2, 8192)) !== false && - $read1 === $read2 && !feof($fp1) && !feof($fp2)) - { - // just loop until we find a difference - } - - fclose($fp1); - fclose($fp2); - //error_log(__METHOD__."($file1, $file2) returning ".array2string($read1 === $read2)." (content differs)"); - return $read1 === $read2; - } -} - -egw_vfs::init_static(); +class egw_vfs extends Vfs {} diff --git a/phpgwapi/inc/class.filesystem_stream_wrapper.inc.php b/phpgwapi/inc/class.filesystem_stream_wrapper.inc.php deleted file mode 100644 index 8be319a0e4..0000000000 --- a/phpgwapi/inc/class.filesystem_stream_wrapper.inc.php +++ /dev/null @@ -1,762 +0,0 @@ - - * @copyright (c) 2008-14 by Ralf Becker - * @version $Id$ - */ - -/** - * eGroupWare API: VFS - stream wrapper to access the regular filesystem (setting a given user, group and mode) - * - * This stream wrapper allows to mount parts of the regular filesystem, under specified permissions. - * You can eg. mount an directory in the docroot to allow the Admin group to upload files there. - * - * This stream wrapper uses query parameters to pass certain options to it: - * - user: uid or user-name owning the path, default root - * - group: gid or group-name owning the path, default root - * - mode: mode bit for the path, default 0005 (read and execute for nobody) - * - exec: false (default) = do NOT allow to upload or modify scripts, true = allow it (if docroot is mounted, this allows to run scripts!) - * scripts are considered every file having a script-extension (eg. .php, .pl, .py), defined with SCRIPT_EXTENSION_PREG constant - * - url: download url, if NOT a regular webdav.php download should be used, eg. because directory already - * lies within the docroot or is mapped via an alias - * - * Example mount command for an uploads directory in the docroot (needs to be writable by webserver!): - * - * filemanager/cli.php mount --user root_admin --password secret --domain default \ - * 'filesystem://egal/var/www/html/uploads?group=Admins&mode=075&url=http://domain.com/uploads' /uploads - * - * (admin / secret is username / password of setup user, "root_" prefix differenciate from regular EGw-user!) - * - * To correctly support characters with special meaning in url's (#?%), we urlencode them with egw_vfs::encodePathComponent - * and urldecode all path again, before passing them to php's filesystem functions. - * - * @link http://www.php.net/manual/en/function.stream-wrapper-register.php - */ -class filesystem_stream_wrapper implements iface_stream_wrapper -{ - /** - * Scheme / protocol used for this stream-wrapper - */ - const SCHEME = 'filesystem'; - /** - * Mime type of directories, the old vfs used 'Directory', while eg. WebDAV uses 'httpd/unix-directory' - */ - const DIR_MIME_TYPE = egw_vfs::DIR_MIME_TYPE ; - - /** - * mode-bits, which have to be set for files - */ - const MODE_FILE = 0100000; - /** - * mode-bits, which have to be set for directories - */ - const MODE_DIR = 040000; - - /** - * optional context param when opening the stream, null if no context passed - * - * @var mixed - */ - var $context; - - /** - * stream / ressouce this class is opened for by stream_open - * - * @var ressource - */ - private $opened_stream; - - /** - * URL of the opened stream, used to build the complete URL of files in the dir - * - * @var string - */ - private $opened_stream_url; - - /** - * directory-ressouce this class is opened for by dir_open - * - * @var ressource - */ - private $opened_dir; - - /** - * URL of the opened dir, used to build the complete URL of files in the dir - * - * @var string - */ - private $opened_dir_url; - - /** - * How much should be logged to the apache error-log - * - * 0 = Nothing - * 1 = only errors - * 2 = all function calls and errors (contains passwords too!) - */ - const LOG_LEVEL = 1; - - /** - * Regular expression identifying scripts, to NOT allow updating them if exec mount option is NOT set - */ - const SCRIPT_EXTENSIONS_PREG = '/\.(php[0-9]*|pl|py)$/'; - - /** - * This method is called immediately after your stream object is created. - * - * @param string $url URL that was passed to fopen() and that this object is expected to retrieve - * @param string $mode mode used to open the file, as detailed for fopen() - * @param int $options additional flags set by the streams API (or'ed together): - * - STREAM_USE_PATH If path is relative, search for the resource using the include_path. - * - STREAM_REPORT_ERRORS If this flag is set, you are responsible for raising errors using trigger_error() during opening of the stream. - * If this flag is not set, you should not raise any errors. - * @param string $opened_path full path of the file/resource, if the open was successfull and STREAM_USE_PATH was set - * @return boolean true if the ressource was opened successful, otherwise false - */ - function stream_open ( $url, $mode, $options, &$opened_path ) - { - $this->opened_stream = $this->opened_stream_url = null; - $read_only = str_replace('b','',$mode) == 'r'; - - // check access rights, based on the eGW mount perms - if (!($stat = self::url_stat($url,0)) || $mode[0] == 'x') // file not found or file should NOT exist - { - $dir = egw_vfs::dirname($url); - if ($mode[0] == 'r' || // does $mode require the file to exist (r,r+) - $mode[0] == 'x' || // or file should not exist, but does - !egw_vfs::check_access($dir,egw_vfs::WRITABLE,$dir_stat=self::url_stat($dir,0))) // or we are not allowed to create it - { - if (self::LOG_LEVEL) error_log(__METHOD__."($url,$mode,$options) file does not exist or can not be created!"); - if (!($options & STREAM_URL_STAT_QUIET)) - { - trigger_error(__METHOD__."($url,$mode,$options) file does not exist or can not be created!",E_USER_WARNING); - } - return false; - } - } - elseif (!$read_only && !egw_vfs::check_access($url,egw_vfs::WRITABLE,$stat)) // we are not allowed to edit it - { - if (self::LOG_LEVEL) error_log(__METHOD__."($url,$mode,$options) file can not be edited!"); - if (!($options & STREAM_URL_STAT_QUIET)) - { - trigger_error(__METHOD__."($url,$mode,$options) file can not be edited!",E_USER_WARNING); - } - return false; - } - if (!$read_only && self::deny_script($url)) - { - if (self::LOG_LEVEL) error_log(__METHOD__."($url,$mode,$options) permission denied, file is a script!"); - if (!($options & STREAM_URL_STAT_QUIET)) - { - trigger_error(__METHOD__."($url,$mode,$options) permission denied, file is a script!",E_USER_WARNING); - } - return false; - } - - // open the "real" file - if (!($this->opened_stream = fopen($path=egw_vfs::decodePath(egw_vfs::parse_url($url,PHP_URL_PATH)),$mode,$options))) - { - if (self::LOG_LEVEL) error_log(__METHOD__."($url,$mode,$options) fopen('$path','$mode',$options) returned false!"); - return false; - } - $this->opened_stream_url = $url; - - return true; - } - - /** - * This method is called when the stream is closed, using fclose(). - * - * You must release any resources that were locked or allocated by the stream. - */ - function stream_close ( ) - { - $ret = fclose($this->opened_stream); - - $this->opened_stream = null; - - return $ret; - } - - /** - * This method is called in response to fread() and fgets() calls on the stream. - * - * You must return up-to count bytes of data from the current read/write position as a string. - * If there are less than count bytes available, return as many as are available. - * If no more data is available, return either FALSE or an empty string. - * You must also update the read/write position of the stream by the number of bytes that were successfully read. - * - * @param int $count - * @return string/false up to count bytes read or false on EOF - */ - function stream_read ( $count ) - { - return fread($this->opened_stream,$count); - } - - /** - * This method is called in response to fwrite() calls on the stream. - * - * You should store data into the underlying storage used by your stream. - * If there is not enough room, try to store as many bytes as possible. - * You should return the number of bytes that were successfully stored in the stream, or 0 if none could be stored. - * You must also update the read/write position of the stream by the number of bytes that were successfully written. - * - * @param string $data - * @return integer - */ - function stream_write ( $data ) - { - return fwrite($this->opened_stream,$data); - } - - /** - * This method is called in response to feof() calls on the stream. - * - * Important: PHP 5.0 introduced a bug that wasn't fixed until 5.1: the return value has to be the oposite! - * - * if(version_compare(PHP_VERSION,'5.0','>=') && version_compare(PHP_VERSION,'5.1','<')) - * { - * $eof = !$eof; - * } - * - * @return boolean true if the read/write position is at the end of the stream and no more data availible, false otherwise - */ - function stream_eof ( ) - { - return feof($this->opened_stream); - } - - /** - * This method is called in response to ftell() calls on the stream. - * - * @return integer current read/write position of the stream - */ - function stream_tell ( ) - { - return ftell($this->opened_stream); - } - - /** - * This method is called in response to fseek() calls on the stream. - * - * You should update the read/write position of the stream according to offset and whence. - * See fseek() for more information about these parameters. - * - * @param integer $offset - * @param integer $whence SEEK_SET - 0 - Set position equal to offset bytes - * SEEK_CUR - 1 - Set position to current location plus offset. - * SEEK_END - 2 - Set position to end-of-file plus offset. (To move to a position before the end-of-file, you need to pass a negative value in offset.) - * @return boolean TRUE if the position was updated, FALSE otherwise. - */ - function stream_seek ( $offset, $whence ) - { - return !fseek($this->opened_stream,$offset,$whence); // fseek returns 0 on success and -1 on failure - } - - /** - * This method is called in response to fflush() calls on the stream. - * - * If you have cached data in your stream but not yet stored it into the underlying storage, you should do so now. - * - * @return booelan TRUE if the cached data was successfully stored (or if there was no data to store), or FALSE if the data could not be stored. - */ - function stream_flush ( ) - { - return fflush($this->opened_stream); - } - - /** - * This method is called in response to fstat() calls on the stream. - * - * If you plan to use your wrapper in a require_once you need to define stream_stat(). - * If you plan to allow any other tests like is_file()/is_dir(), you have to define url_stat(). - * stream_stat() must define the size of the file, or it will never be included. - * url_stat() must define mode, or is_file()/is_dir()/is_executable(), and any of those functions affected by clearstatcache() simply won't work. - * It's not documented, but directories must be a mode like 040777 (octal), and files a mode like 0100666. - * If you wish the file to be executable, use 7s instead of 6s. - * The last 3 digits are exactly the same thing as what you pass to chmod. - * 040000 defines a directory, and 0100000 defines a file. - * - * @return array containing the same values as appropriate for the stream. - */ - function stream_stat ( ) - { - return self::url_stat($this->opened_stream_url,0); - } - - /** - * This method is called in response to unlink() calls on URL paths associated with the wrapper. - * - * It should attempt to delete the item specified by path. - * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support unlinking! - * - * @param string $url - * @return boolean TRUE on success or FALSE on failure - */ - static function unlink ( $url ) - { - $path = egw_vfs::decodePath(egw_vfs::parse_url($url,PHP_URL_PATH)); - - // check access rights (file need to exist and directory need to be writable - if (!file_exists($path) || is_dir($path) || !egw_vfs::check_access(egw_vfs::dirname($url),egw_vfs::WRITABLE)) - { - if (self::LOG_LEVEL) error_log(__METHOD__."($url) permission denied!"); - return false; // no permission or file does not exist - } - return unlink($path); - } - - /** - * This method is called in response to rename() calls on URL paths associated with the wrapper. - * - * It should attempt to rename the item specified by path_from to the specification given by path_to. - * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support renaming. - * - * The regular filesystem stream-wrapper returns an error, if $url_from and $url_to are not either both files or both dirs! - * - * @param string $url_from - * @param string $url_to - * @return boolean TRUE on success or FALSE on failure - */ - static function rename ( $url_from, $url_to ) - { - $from = egw_vfs::parse_url($url_from); - $to = egw_vfs::parse_url($url_to); - - // check access rights - if (!($from_stat = self::url_stat($url_from,0)) || !egw_vfs::check_access(egw_vfs::dirname($url_from),egw_vfs::WRITABLE)) - { - if (self::LOG_LEVEL) error_log(__METHOD__."($url_from,$url_to): $from[path] permission denied!"); - return false; // no permission or file does not exist - } - $to_dir = egw_vfs::dirname($url_to); - if (!egw_vfs::check_access($to_dir,egw_vfs::WRITABLE,$to_dir_stat = self::url_stat($to_dir,0))) - { - if (self::LOG_LEVEL) error_log(__METHOD__."($url_from,$url_to): $to_dir permission denied!"); - return false; // no permission or parent-dir does not exist - } - if (self::deny_script($url_to)) - { - if (self::LOG_LEVEL) error_log(__METHOD__."($url_from,$url_to) permission denied, file is a script!"); - return false; - } - // the filesystem stream-wrapper does NOT allow to rename files to directories, as this makes problems - // for our vfs too, we abort here with an error, like the filesystem one does - if (($to_stat = self::url_stat($to['path'],0)) && - ($to_stat['mime'] === self::DIR_MIME_TYPE) !== ($from_stat['mime'] === self::DIR_MIME_TYPE)) - { - $is_dir = $to_stat['mime'] === self::DIR_MIME_TYPE ? 'a' : 'no'; - if (self::LOG_LEVEL) error_log(__METHOD__."($url_to,$url_from) $to[path] is $is_dir directory!"); - return false; // no permission or file does not exist - } - // if destination file already exists, delete it - if ($to_stat && !self::unlink($url_to,$operation)) - { - if (self::LOG_LEVEL) error_log(__METHOD__."($url_to,$url_from) can't unlink existing $url_to!"); - return false; - } - return rename(egw_vfs::decodePath($from['path']),egw_vfs::decodePath($to['path'])); - } - - /** - * This method is called in response to mkdir() calls on URL paths associated with the wrapper. - * - * It should attempt to create the directory specified by path. - * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support creating directories. - * - * @param string $url - * @param int $mode not used, as we dont allow to change mode - * @param int $options Posible values include STREAM_REPORT_ERRORS and STREAM_MKDIR_RECURSIVE - * @return boolean TRUE on success or FALSE on failure - */ - static function mkdir ( $url, $mode, $options ) - { - $path = egw_vfs::decodePath(egw_vfs::parse_url($url,PHP_URL_PATH)); - $recursive = (bool)($options & STREAM_MKDIR_RECURSIVE); - - // find the real parent (might be more then one level if $recursive!) - do { - $parent = dirname($parent ? $parent : $path); - $parent_url = egw_vfs::dirname($parent_url ? $parent_url : $url); - } - while ($recursive && $parent != '/' && !file_exists($parent)); - //echo __METHOD__."($url,$mode,$options) path=$path, recursive=$recursive, parent=$parent, egw_vfs::check_access(parent_url=$parent_url,egw_vfs::WRITABLE)=".(int)egw_vfs::check_access($parent_url,egw_vfs::WRITABLE)."\n"; - - // check access rights (in real filesystem AND by mount perms) - if (file_exists($path) || !file_exists($parent) || !is_writable($parent) || !egw_vfs::check_access($parent_url,egw_vfs::WRITABLE)) - { - if (self::LOG_LEVEL) error_log(__METHOD__."($url) permission denied!"); - return false; - } - return mkdir($path,$mode=0700,$recursive); // setting mode 0700 allows (only) apache to write into the dir - } - - /** - * This method is called in response to rmdir() calls on URL paths associated with the wrapper. - * - * It should attempt to remove the directory specified by path. - * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support removing directories. - * - * @param string $url - * @param int $options Possible values include STREAM_REPORT_ERRORS. - * @return boolean TRUE on success or FALSE on failure. - */ - static function rmdir ( $url, $options ) - { - $path = egw_vfs::decodePath(egw_vfs::parse_url($url,PHP_URL_PATH)); - $parent = dirname($path); - - // check access rights (in real filesystem AND by mount perms) - if (!file_exists($path) || !is_writable($parent) || !egw_vfs::check_access(egw_vfs::dirname($url),egw_vfs::WRITABLE)) - { - if (self::LOG_LEVEL) error_log(__METHOD__."($url) permission denied!"); - return false; - } - return rmdir($path); - } - - /** - * This is not (yet) a stream-wrapper function, but it's necessary and can be used static - * - * @param string $path - * @param int $time=null modification time (unix timestamp), default null = current time - * @param int $atime=null access time (unix timestamp), default null = current time, not implemented in the vfs! - * @return boolean true on success, false otherwise - */ - static function touch($url,$time=null,$atime=null) - { - $path = egw_vfs::decodePath(egw_vfs::parse_url($url,PHP_URL_PATH)); - $parent = dirname($path); - - // check access rights (in real filesystem AND by mount perms) - if (!file_exists($path) || !is_writable($parent) || !egw_vfs::check_access(egw_vfs::dirname($url),egw_vfs::WRITABLE)) - { - if (self::LOG_LEVEL) error_log(__METHOD__."($url) permission denied!"); - return false; - } - return touch($path,$time,$atime); - } - - /** - * This is not (yet) a stream-wrapper function, but it's necessary and can be used static - * - * Not supported, as it would require root rights! - * - * @param string $path - * @param string $mode mode string see egw_vfs::mode2int - * @return boolean true on success, false otherwise - */ - static function chmod($path,$mode) - { - return false; - } - - /** - * This is not (yet) a stream-wrapper function, but it's necessary and can be used static - * - * Not supported, as it would require root rights! - * - * @param string $path - * @param int $owner numeric user id - * @return boolean true on success, false otherwise - */ - static function chown($path,$owner) - { - return false; - } - - /** - * This is not (yet) a stream-wrapper function, but it's necessary and can be used static - * - * Not supported, as it would require root rights! - * - * @param string $path - * @param int $group numeric group id - * @return boolean true on success, false otherwise - */ - static function chgrp($path,$group) - { - return false; - } - - /** - * This method is called immediately when your stream object is created for examining directory contents with opendir(). - * - * @param string $url URL that was passed to opendir() and that this object is expected to explore. - * @param int $options - * @return booelan - */ - function dir_opendir ( $url, $options ) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url,$options)"); - - $this->opened_dir = null; - - $path = egw_vfs::decodePath(egw_vfs::parse_url($this->opened_dir_url = $url,PHP_URL_PATH)); - - // ToDo: check access rights - - if (!($this->opened_dir = opendir($path))) - { - if (self::LOG_LEVEL > 0) error_log(__METHOD__."($url,$options) opendir('$path') failed!"); - return false; - } - return true; - } - - /** - * This method is called in response to stat() calls on the URL paths associated with the wrapper. - * - * It should return as many elements in common with the system function as possible. - * Unknown or unavailable values should be set to a rational value (usually 0). - * - * If you plan to use your wrapper in a require_once you need to define stream_stat(). - * If you plan to allow any other tests like is_file()/is_dir(), you have to define url_stat(). - * stream_stat() must define the size of the file, or it will never be included. - * url_stat() must define mode, or is_file()/is_dir()/is_executable(), and any of those functions affected by clearstatcache() simply won't work. - * It's not documented, but directories must be a mode like 040777 (octal), and files a mode like 0100666. - * If you wish the file to be executable, use 7s instead of 6s. - * The last 3 digits are exactly the same thing as what you pass to chmod. - * 040000 defines a directory, and 0100000 defines a file. - * - * @param string $url - * @param int $flags holds additional flags set by the streams API. It can hold one or more of the following values OR'd together: - * - STREAM_URL_STAT_LINK For resources with the ability to link to other resource (such as an HTTP Location: forward, - * or a filesystem symlink). This flag specified that only information about the link itself should be returned, - * not the resource pointed to by the link. - * This flag is set in response to calls to lstat(), is_link(), or filetype(). - * - STREAM_URL_STAT_QUIET If this flag is set, your wrapper should not raise any errors. If this flag is not set, - * you are responsible for reporting errors using the trigger_error() function during stating of the path. - * stat triggers it's own warning anyway, so it makes no sense to trigger one by our stream-wrapper! - * @return array - */ - static function url_stat ( $url, $flags ) - { - $parts = egw_vfs::parse_url($url); - $path = egw_vfs::decodePath($parts['path']); - - $stat = @stat($path); // suppressed the stat failed warnings - - if ($stat) - { - // set owner, group and mode from mount options - if (!self::parse_query($parts['query'],$uid,$gid,$mode)) - { - return false; - if (self::LOG_LEVEL > 0) error_log(__METHOD__."($url,$flags) can NOT self::parse_query('$parts[query]')!"); - } - $stat['uid'] = $stat[4] = $uid; - $stat['gid'] = $stat[5] = $gid; - $stat['mode'] = $stat[2] = $stat['mode'] & self::MODE_DIR ? self::MODE_DIR | $mode : self::MODE_FILE | ($mode & ~0111); - // write rights also depend on the write rights of the webserver - if (!is_writable($path)) - { - $stat['mode'] = $stat[2] = $stat['mode'] & ~0222; - } - } - if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url,$flags) path=$path, mount_mode=".sprintf('0%o',$mode).", mode=".sprintf('0%o',$stat['mode']).'='.egw_vfs::int2mode($stat['mode'])); - return $stat; - } - - /** - * This method is called in response to readdir(). - * - * It should return a string representing the next filename in the location opened by dir_opendir(). - * - * Unless other filesystem, we only return files readable by the user, if the dir is not writable for him. - * This is done to hide files and dirs not accessible by the user (eg. other peoples home-dirs in /home). - * - * @return string - */ - function dir_readdir ( ) - { - do { - $file = readdir($this->opened_dir); - - $ignore = !($file === false || // stop if no more dirs or - ($file != '.' && $file != '..' )); // file not . or .. - if (self::LOG_LEVEL > 1 && $ignore) error_log(__METHOD__.'() ignoring '.array2string($file)); - } - while ($ignore); - - // encode special chars messing up url's - if ($file !== false) $file = egw_vfs::encodePathComponent($file); - - if (self::LOG_LEVEL > 1) error_log(__METHOD__.'() returning '.array2string($file)); - - return $file; - } - - /** - * This method is called in response to rewinddir(). - * - * It should reset the output generated by dir_readdir(). i.e.: - * The next call to dir_readdir() should return the first entry in the location returned by dir_opendir(). - * - * @return boolean - */ - function dir_rewinddir ( ) - { - return rewinddir($this->opened_dir); - } - - /** - * This method is called in response to closedir(). - * - * You should release any resources which were locked or allocated during the opening and use of the directory stream. - * - * @return boolean - */ - function dir_closedir ( ) - { - closedir($this->opened_dir); - - $this->opened_dir = $this->extra_dirs = null; - - return true; - } - - /** - * parse a query containing mount parameters: user, uid, group, gid or mode - * - * @param string|array $query query string or array returned by parse_url (key 'query' holds the value) - * @param int &$uid default if not set is 0=root - * @param int &$gid default if not set is 0=root - * @param int &$mode default if not set is 05 r-x for others - * @return boolean true on successfull parse, false on error - */ - static function parse_query($query,&$uid,&$gid,&$mode) - { - parse_str(is_array($query) ? $query['query'] : $query,$params); - - // setting the default perms root.root r-x for other - $uid = $gid = 0; - $mode = 05; - - foreach($params as $name => $value) - { - switch($name) - { - case 'user': - if (!is_numeric($value)) - { - if ($name === 'root') - { - $value = 0; - } - elseif (($value = $GLOBALS['egw']->accounts->name2id($value,'account_lid','u')) === false) - { - error_log(__METHOD__."('$query') unknown user-name '$value'!"); - return false; // wrong user-name - } - } - // fall-through - case 'uid': - if (!is_numeric($value) || $value < 0 || !is_int($value) && !$GLOBALS['egw']->accounts->id2name($value)) - { - error_log(__METHOD__."('$query') wrong numeric user-id '$value'!"); - return false; - } - $uid = (int)$value; - break; - case 'group': - if (!is_numeric($value)) - { - if ($name === 'root') - { - $value = 0; - } - elseif (($value = $GLOBALS['egw']->accounts->name2id($value,'account_lid','g')) === false) - { - error_log(__METHOD__."('$query') unknown group-name '$value'!"); - return false; // wrong group-name - } - $value = -$value; // vfs uses positiv gid's! - } - // fall-through - case 'gid': - if (!is_numeric($value) || $value < 0 || !is_int($value) && !$GLOBALS['egw']->accounts->id2name(-$value)) - { - error_log(__METHOD__."('$query') wrong numeric group-id '$value'!"); - return false; - } - $gid = (int)$value; - break; - case 'mode': - $mode = egw_vfs::mode2int($value); - break; - case 'url': - // ignored, only used for download_url method - break; - default: - error_log(__METHOD__."('$query') unknown option '$name'!"); - break; - } - } - //echo __METHOD__.'('.print_r($query,true).") uid=$uid, gid=$gid, mode=".sprintf('0%o',$mode)."\n"; - return true; - } - - /** - * Check if url is a script (self::$script_extentions) and exec mount option is NOT set - * - * @param string $url - * @return boolean true if $url is a script AND exec is NOT set, false otherwise - */ - static function deny_script($url) - { - $parts = egw_vfs::parse_url($url); - parse_str($parts['query'],$get); - - $deny = !$get['exec'] && preg_match(self::SCRIPT_EXTENSIONS_PREG,$parts['path']); - - if (self::LOG_LEVEL > 1 || self::LOG_LEVEL > 0 && $deny) - { - error_log(__METHOD__."($url) returning ".array2string($deny)); - } - return $deny; - } - - /** - * URL to download a file - * - * We use our webdav handler as download url instead of an own download method. - * The webdav hander (filemanager/webdav.php) recognices eGW's session cookie and of cause understands regular GET requests. - * - * @param string $url - * @param boolean $force_download=false add header('Content-disposition: filename="' . basename($path) . '"'), currently not supported! - * @todo get $force_download working through webdav - * @return string|false string with full download url or false to use default webdav.php url - */ - static function download_url($url,$force_download=false) - { - list(,$query) = explode('?',$url,2); - parse_str($query,$get); - if (empty($get['url'])) return false; // no download url given for this mount-point - - if (!($mount_url = egw_vfs::mount_url($url))) return false; // no mount url found, should not happen - list($mount_url) = explode('?',$mount_url); - - list($url,$query) = explode('?',$url,2); - $relpath = substr($url,strlen($mount_url)); - - $download_url = egw_vfs::concat($get['url'],$relpath); - if ($download_url[0] == '/') - { - $download_url = ($_SERVER['HTTPS'] ? 'https://' : 'http://'). - $_SERVER['HTTP_HOST'].$download_url; - } - - //die(__METHOD__."('$url') --> relpath = $relpath --> $download_url"); - return $download_url; - } -} - -stream_register_wrapper(filesystem_stream_wrapper::SCHEME ,'filesystem_stream_wrapper'); diff --git a/phpgwapi/inc/class.iface_stream_wrapper.inc.php b/phpgwapi/inc/class.iface_stream_wrapper.inc.php index 48da3c762a..ed8c778bec 100644 --- a/phpgwapi/inc/class.iface_stream_wrapper.inc.php +++ b/phpgwapi/inc/class.iface_stream_wrapper.inc.php @@ -1,6 +1,6 @@ =') && version_compare(PHP_VERSION,'5.1','<')) - * { - * $eof = !$eof; - * } - * - * @return boolean true if the read/write position is at the end of the stream and no more data availible, false otherwise - */ - function stream_eof ( ); - - /** - * This method is called in response to ftell() calls on the stream. - * - * @return integer current read/write position of the stream - */ - function stream_tell ( ); - - /** - * This method is called in response to fseek() calls on the stream. - * - * You should update the read/write position of the stream according to offset and whence. - * See fseek() for more information about these parameters. - * - * @param integer $offset - * @param integer $whence SEEK_SET - Set position equal to offset bytes - * SEEK_CUR - Set position to current location plus offset. - * SEEK_END - Set position to end-of-file plus offset. (To move to a position before the end-of-file, you need to pass a negative value in offset.) - * @return boolean TRUE if the position was updated, FALSE otherwise. - */ - function stream_seek ( $offset, $whence ); - - /** - * This method is called in response to fflush() calls on the stream. - * - * If you have cached data in your stream but not yet stored it into the underlying storage, you should do so now. - * - * @return booelan TRUE if the cached data was successfully stored (or if there was no data to store), or FALSE if the data could not be stored. - */ - function stream_flush ( ); - - /** - * This method is called in response to fstat() calls on the stream. - * - * If you plan to use your wrapper in a require_once you need to define stream_stat(). - * If you plan to allow any other tests like is_file()/is_dir(), you have to define url_stat(). - * stream_stat() must define the size of the file, or it will never be included. - * url_stat() must define mode, or is_file()/is_dir()/is_executable(), and any of those functions affected by clearstatcache() simply won't work. - * It's not documented, but directories must be a mode like 040777 (octal), and files a mode like 0100666. - * If you wish the file to be executable, use 7s instead of 6s. - * The last 3 digits are exactly the same thing as what you pass to chmod. - * 040000 defines a directory, and 0100000 defines a file. - * - * @return array containing the same values as appropriate for the stream. - */ - function stream_stat ( ); - - /** - * This method is called in response to unlink() calls on URL paths associated with the wrapper. - * - * It should attempt to delete the item specified by path. - * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support unlinking! - * - * @param string $path - * @return boolean TRUE on success or FALSE on failure - */ - static function unlink ( $path ); - - /** - * This method is called in response to rename() calls on URL paths associated with the wrapper. - * - * It should attempt to rename the item specified by path_from to the specification given by path_to. - * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support renaming. - * - * The regular filesystem stream-wrapper returns an error, if $url_from and $url_to are not either both files or both dirs! - * - * @param string $path_from - * @param string $path_to - * @return boolean TRUE on success or FALSE on failure - */ - static function rename ( $path_from, $path_to ); - - /** - * This method is called in response to mkdir() calls on URL paths associated with the wrapper. - * - * It should attempt to create the directory specified by path. - * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support creating directories. - * - * @param string $path - * @param int $mode - * @param int $options Posible values include STREAM_REPORT_ERRORS and STREAM_MKDIR_RECURSIVE - * @return boolean TRUE on success or FALSE on failure - */ - static function mkdir ( $path, $mode, $options ); - - /** - * This method is called in response to rmdir() calls on URL paths associated with the wrapper. - * - * It should attempt to remove the directory specified by path. - * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support removing directories. - * - * @param string $path - * @param int $options Possible values include STREAM_REPORT_ERRORS. - * @return boolean TRUE on success or FALSE on failure. - */ - static function rmdir ( $path, $options ); - - /** - * This method is called immediately when your stream object is created for examining directory contents with opendir(). - * - * @param string $path URL that was passed to opendir() and that this object is expected to explore. - * @return booelan - */ - function dir_opendir ( $path, $options ); - - /** - * This method is called in response to stat() calls on the URL paths associated with the wrapper. - * - * It should return as many elements in common with the system function as possible. - * Unknown or unavailable values should be set to a rational value (usually 0). - * - * If you plan to use your wrapper in a require_once you need to define stream_stat(). - * If you plan to allow any other tests like is_file()/is_dir(), you have to define url_stat(). - * stream_stat() must define the size of the file, or it will never be included. - * url_stat() must define mode, or is_file()/is_dir()/is_executable(), and any of those functions affected by clearstatcache() simply won't work. - * It's not documented, but directories must be a mode like 040777 (octal), and files a mode like 0100666. - * If you wish the file to be executable, use 7s instead of 6s. - * The last 3 digits are exactly the same thing as what you pass to chmod. - * 040000 defines a directory, and 0100000 defines a file. - * - * @param string $path - * @param int $flags holds additional flags set by the streams API. It can hold one or more of the following values OR'd together: - * - STREAM_URL_STAT_LINK For resources with the ability to link to other resource (such as an HTTP Location: forward, - * or a filesystem symlink). This flag specified that only information about the link itself should be returned, - * not the resource pointed to by the link. - * This flag is set in response to calls to lstat(), is_link(), or filetype(). - * - STREAM_URL_STAT_QUIET If this flag is set, your wrapper should not raise any errors. If this flag is not set, - * you are responsible for reporting errors using the trigger_error() function during stating of the path. - * stat triggers it's own warning anyway, so it makes no sense to trigger one by our stream-wrapper! - * @return array - */ - static function url_stat ( $path, $flags ); - - /** - * This method is called in response to readdir(). - * - * It should return a string representing the next filename in the location opened by dir_opendir(). - * - * @return string - */ - function dir_readdir ( ); - - /** - * This method is called in response to rewinddir(). - * - * It should reset the output generated by dir_readdir(). i.e.: - * The next call to dir_readdir() should return the first entry in the location returned by dir_opendir(). - * - * @return boolean - */ - function dir_rewinddir ( ); - - /** - * This method is called in response to closedir(). - * - * You should release any resources which were locked or allocated during the opening and use of the directory stream. - * - * @return boolean - */ - function dir_closedir ( ); -} +interface iface_stream_wrapper extends Vfs\StreamWrapperIface {} diff --git a/phpgwapi/inc/class.links_stream_wrapper.inc.php b/phpgwapi/inc/class.links_stream_wrapper.inc.php deleted file mode 100644 index 5ebeb33dc7..0000000000 --- a/phpgwapi/inc/class.links_stream_wrapper.inc.php +++ /dev/null @@ -1,321 +0,0 @@ - - * @copyright (c) 2008-14 by Ralf Becker - * @version $Id: class.sqlfs_stream_wrapper.inc.php 24997 2008-03-02 21:44:15Z ralfbecker $ - */ - -/** - * Define parent for links_stream_wrapper, if not already defined - * - * Allows to base links_stream_wrapper on an other wrapper - */ -if (!class_exists('links_stream_wrapper_parent',false)) -{ - class links_stream_wrapper_parent extends sqlfs_stream_wrapper {} -} - -/** - * EGroupware API: stream wrapper for linked files - * - * The files stored by the sqlfs_stream_wrapper in a /apps/$app/$id directory - * - * The links stream wrapper extends the sqlfs one, to implement an own ACL based on the access - * of the entry the files are linked to. - * - * Applications can define a 'file_access' method in the link registry with the following signature: - * - * boolean function file_access(string $id,int $check,string $rel_path) - * - * If the do not implement such a function the title function is used to test if the user has - * at least read access to an entry, and if true full (write) access to the files is granted. - * - * Entry directories are always reported existing and empty, if not existing in sqlfs. - * - * The stream wrapper interface is according to the docu on php.net - * - * @link http://www.php.net/manual/en/function.stream-wrapper-register.php - */ -class links_stream_wrapper extends links_stream_wrapper_parent -{ - /** - * Scheme / protocoll used for this stream-wrapper - */ - const SCHEME = 'links'; - /** - * Prefix to predend to get an url from a path - */ - const PREFIX = 'links://default'; - /** - * Base url to store links - */ - const BASEURL = 'links://default/apps'; - /** - * Enable some debug output to the error_log - */ - const DEBUG = false; - - /** - * Implements ACL based on the access of the user to the entry the files are linked to. - * - * @param string $url url to check - * @param int $check mode to check: one or more or'ed together of: 4 = read, 2 = write, 1 = executable - * @return boolean - */ - static function check_extended_acl($url,$check) - { - if (egw_vfs::$is_root) - { - return true; - } - $path = egw_vfs::parse_url($url,PHP_URL_PATH); - - list(,$apps,$app,$id,$rel_path) = explode('/',$path,5); - - if ($apps != 'apps') - { - $access = false; // no access to anything, but /apps - $what = '!= apps'; - } - elseif (!$app) - { - $access = !($check & egw_vfs::WRITABLE); // always grant read access to /apps - $what = '!$app'; - } - elseif(!isset($GLOBALS['egw_info']['user']['apps'][$app])) - { - $access = false; // user has no access to the $app application - $what = 'no app-rights'; - } - elseif (!$id) - { - $access = true; // grant read&write access to /apps/$app - $what = 'app dir'; - } - // allow applications to implement their own access control to the file storage - // otherwise use the title method to check if user has (at least read access) to the entry - // which gives him then read AND write access to the file store of the entry - else - { - // vfs & stream-wrapper use posix rights, egw_link::file_access uses EGW_ACL_{EDIT|READ}! - $required = $check & egw_vfs::WRITABLE ? EGW_ACL_EDIT : EGW_ACL_READ; - $access = egw_link::file_access($app,$id,$required,$rel_path,egw_vfs::$user); - $what = "from egw_link::file_access('$app',$id,$required,'$rel_path,".egw_vfs::$user.")"; - } - if (self::DEBUG) error_log(__METHOD__."($url,$check) user=".egw_vfs::$user." ($what) ".($access?"access granted ($app:$id:$rel_path)":'no access!!!')); - return $access; - } - - /** - * This method is called in response to stat() calls on the URL paths associated with the wrapper. - * - * Reimplemented from sqlfs, as we have to pass the value of check_extends_acl(), due to the lack of late static binding. - * And to return vcard for url /apps/addressbook/$id/.entry - * - * @param string $path - * @param int $flags holds additional flags set by the streams API. It can hold one or more of the following values OR'd together: - * - STREAM_URL_STAT_LINK For resources with the ability to link to other resource (such as an HTTP Location: forward, - * or a filesystem symlink). This flag specified that only information about the link itself should be returned, - * not the resource pointed to by the link. - * This flag is set in response to calls to lstat(), is_link(), or filetype(). - * - STREAM_URL_STAT_QUIET If this flag is set, your wrapper should not raise any errors. If this flag is not set, - * you are responsible for reporting errors using the trigger_error() function during stating of the path. - * stat triggers it's own warning anyway, so it makes no sense to trigger one by our stream-wrapper! - * @return array - */ - static function url_stat ( $url, $flags ) - { - $eacl_check=self::check_extended_acl($url,egw_vfs::READABLE); - - // return vCard as /.entry - if ( $eacl_check && substr($url,-7) == '/.entry' && - (list($app) = array_slice(explode('/',$url),-3,1)) && $app === 'addressbook') - { - $ret = array( - 'ino' => md5($url), - 'name' => '.entry', - 'mode' => self::MODE_FILE|egw_vfs::READABLE, // required by the stream wrapper - 'size' => 1024, // fmail does NOT attach files with size 0! - 'uid' => 0, - 'gid' => 0, - 'mtime' => time(), - 'ctime' => time(), - 'nlink' => 1, - // eGW addition to return some extra values - 'mime' => $app == 'addressbook' ? 'text/vcard' : 'text/calendar', - ); - } - // if entry directory does not exist --> return fake directory - elseif (!($ret = parent::url_stat($url,$flags,$eacl_check)) && $eacl_check) - { - list(,/*$apps*/,/*$app*/,$id,$rel_path) = explode('/', egw_vfs::parse_url($url, PHP_URL_PATH), 5); - if ($id && !isset($rel_path)) - { - $ret = array( - 'ino' => md5($url), - 'name' => $id, - 'mode' => self::MODE_DIR, // required by the stream wrapper - 'size' => 0, - 'uid' => 0, - 'gid' => 0, - 'mtime' => time(), - 'ctime' => time(), - 'nlink' => 2, - // eGW addition to return some extra values - 'mime' => egw_vfs::DIR_MIME_TYPE, - ); - } - } - if (self::DEBUG) error_log(__METHOD__."('$url', $flags) calling parent::url_stat(,,".array2string($eacl_check).') returning '.array2string($ret)); - return $ret; - } - - /** - * Set or delete extended acl for a given path and owner (or delete them if is_null($rights) - * - * Reimplemented, to NOT call the sqlfs functions, as we dont allow to modify the ACL (defined by the apps) - * - * @param string $path string with path - * @param int $rights=null rights to set, or null to delete the entry - * @param int/boolean $owner=null owner for whom to set the rights, null for the current user, or false to delete all rights for $path - * @param int $fs_id=null fs_id to use, to not query it again (eg. because it's already deleted) - * @return boolean true if acl is set/deleted, false on error - */ - static function eacl($path,$rights=null,$owner=null,$fs_id=null) - { - unset($path, $rights, $owner, $fs_id); // not used, but required by function signature - - return false; - } - - /** - * Get all ext. ACL set for a path - * - * Reimplemented, to NOT call the sqlfs functions, as we dont allow to modify the ACL (defined by the apps) - * - * @param string $path - * @return array/boolean array with array('path'=>$path,'owner'=>$owner,'rights'=>$rights) or false if $path not found - */ - function get_eacl($path) - { - unset($path); // not used, but required by function signature - - return false; - } - - /** - * mkdir for links - * - * Reimplemented as we have no static late binding to allow the extended sqlfs to call our eacl and to set no default rights for entry dirs - * - * This method is called in response to mkdir() calls on URL paths associated with the wrapper. - * - * It should attempt to create the directory specified by path. - * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support creating directories. - * - * @param string $path - * @param int $mode not used(!), we inherit 005 for /apps/$app and set 000 for /apps/$app/$id - * @param int $options Posible values include STREAM_REPORT_ERRORS and STREAM_MKDIR_RECURSIVE, we allways use recursive! - * @return boolean TRUE on success or FALSE on failure - */ - static function mkdir($path,$mode,$options) - { - unset($mode); // not used, but required by function signature - - if($path[0] != '/') - { - if (strpos($path,'?') !== false) $query = egw_vfs::parse_url($path,PHP_URL_QUERY); - $path = egw_vfs::parse_url($path,PHP_URL_PATH).($query ? '?'.$query : ''); - } - list(,$apps,$app,$id) = explode('/',$path); - - $ret = false; - if ($apps == 'apps' && $app && !$id || self::check_extended_acl($path,egw_vfs::WRITABLE)) // app directory itself is allways ok - { - $current_is_root = egw_vfs::$is_root; egw_vfs::$is_root = true; - $current_user = egw_vfs::$user; egw_vfs::$user = 0; - - $ret = parent::mkdir($path,0,$options|STREAM_MKDIR_RECURSIVE); - if ($id) parent::chmod($path,0); // no other rights - - egw_vfs::$user = $current_user; - egw_vfs::$is_root = $current_is_root; - } - //error_log(__METHOD__."($path,$mode,$options) apps=$apps, app=$app, id=$id: returning $ret"); - return $ret; - } - - /** - * This method is called immediately after your stream object is created. - * - * Reimplemented from sqlfs to ensure self::url_stat is called, to fill sqlfs stat cache with our eacl! - * And to return vcard for url /apps/addressbook/$id/.entry - * - * @param string $url URL that was passed to fopen() and that this object is expected to retrieve - * @param string $mode mode used to open the file, as detailed for fopen() - * @param int $options additional flags set by the streams API (or'ed together): - * - STREAM_USE_PATH If path is relative, search for the resource using the include_path. - * - STREAM_REPORT_ERRORS If this flag is set, you are responsible for raising errors using trigger_error() during opening of the stream. - * If this flag is not set, you should not raise any errors. - * @param string $opened_path full path of the file/resource, if the open was successfull and STREAM_USE_PATH was set - * @return boolean true if the ressource was opened successful, otherwise false - */ - function stream_open ( $url, $mode, $options, &$opened_path ) - { - // the following call is necessary to fill sqlfs_stream_wrapper::$stat_cache, WITH the extendes ACL! - $stat = self::url_stat($url,0); - //error_log(__METHOD__."('$url', '$mode', $options) stat=".array2string($stat)); - - // return vCard as /.entry - if ($stat && $mode[0] == 'r' && substr($url,-7) === '/.entry' && - (list($app) = array_slice(explode('/',$url),-3,1)) && $app === 'addressbook') - { - list($id) = array_slice(explode('/',$url),-2,1); - $name = md5($url); - $ab_vcard = new addressbook_vcal('addressbook','text/vcard'); - if (!($GLOBALS[$name] =& $ab_vcard->getVCard($id))) - { - error_log(__METHOD__."('$url', '$mode', $options) addressbook_vcal::getVCard($id) returned false!"); - return false; - } - //error_log(__METHOD__."('$url', '$mode', $options) addressbook_vcal::getVCard($id) returned ".$GLOBALS[$name]); - require_once(EGW_API_INC.'/class.global_stream_wrapper.inc.php'); - $this->opened_stream = fopen('global://'.$name,'r'); - unset($GLOBALS[$name]); // unset it, so it does not use up memory, once the stream is closed - return true; - } - // create not existing entry directories on the fly - if ($mode[0] != 'r' && !parent::url_stat($dir = egw_vfs::dirname($url),0) && self::check_extended_acl($dir,egw_vfs::WRITABLE)) - { - self::mkdir($dir,0,STREAM_MKDIR_RECURSIVE); - } - return parent::stream_open($url,$mode,$options,$opened_path); - } - - /** - * This method is called immediately when your stream object is created for examining directory contents with opendir(). - * - * Reimplemented to give no error, if entry directory does not exist. - * - * @param string $path URL that was passed to opendir() and that this object is expected to explore. - * @param $options - * @return booelan - */ - function dir_opendir ( $url, $options ) - { - if (!parent::url_stat($url, STREAM_URL_STAT_QUIET) && self::url_stat($url, STREAM_URL_STAT_QUIET)) - { - $this->opened_dir = array(); - return true; - } - return parent::dir_opendir($url, $options); - } -} - -stream_register_wrapper(links_stream_wrapper::SCHEME ,'links_stream_wrapper'); diff --git a/phpgwapi/inc/class.sqlfs_stream_wrapper.inc.php b/phpgwapi/inc/class.sqlfs_stream_wrapper.inc.php index 6b83baa41a..da45133c11 100644 --- a/phpgwapi/inc/class.sqlfs_stream_wrapper.inc.php +++ b/phpgwapi/inc/class.sqlfs_stream_wrapper.inc.php @@ -7,1915 +7,13 @@ * @package api * @subpackage vfs * @author Ralf Becker - * @copyright (c) 2008-14 by Ralf Becker + * @copyright (c) 2008-15 by Ralf Becker * @version $Id$ */ +use EGroupware\Api\Vfs\Sqlfs; + /** - * EGroupware API: VFS - new DB based VFS stream wrapper - * - * The sqlfs stream wrapper has 2 operation modi: - * - content of files is stored in the filesystem (eGW's files_dir) (default) - * - content of files is stored as BLOB in the DB (can be enabled by mounting sqlfs://...?storage=db) - * please note the current (php5.2.6) problems: - * a) retriving files via streams does NOT work for PDO_mysql (bindColum(,,PDO::PARAM_LOB) does NOT work, string returned) - * (there's a workaround implemented, but it requires to allocate memory for the whole file!) - * b) uploading/writing files > 1M fail on PDOStatement::execute() (setting PDO::MYSQL_ATTR_MAX_BUFFER_SIZE does NOT help) - * (not sure if that's a bug in PDO/PDO_mysql or an accepted limitation) - * - * I use the PDO DB interface, as it allows to access BLOB's as streams (avoiding to hold them complete in memory). - * - * The stream wrapper interface is according to the docu on php.net - * - * @link http://www.php.net/manual/en/function.stream-wrapper-register.php + * @depredated use EGroupware\Api\Vfs\Sqlfs\StreamWrapper */ -class sqlfs_stream_wrapper implements iface_stream_wrapper -{ - /** - * Mime type of directories, the old vfs uses 'Directory', while eg. WebDAV uses 'httpd/unix-directory' - */ - const DIR_MIME_TYPE = 'httpd/unix-directory'; - /** - * Mime type for symlinks - */ - const SYMLINK_MIME_TYPE = 'application/x-symlink'; - /** - * Scheme / protocoll used for this stream-wrapper - */ - const SCHEME = 'sqlfs'; - /** - * Does url_stat returns a mime type, or has it to be determined otherwise (string with attribute name) - */ - const STAT_RETURN_MIME_TYPE = 'mime'; - /** - * Our tablename - */ - const TABLE = 'egw_sqlfs'; - /** - * Name of our property table - */ - const PROPS_TABLE = 'egw_sqlfs_props'; - /** - * mode-bits, which have to be set for files - */ - const MODE_FILE = 0100000; - /** - * mode-bits, which have to be set for directories - */ - const MODE_DIR = 040000; - /** - * mode-bits, which have to be set for links - */ - const MODE_LINK = 0120000; - /** - * How much should be logged to the apache error-log - * - * 0 = Nothing - * 1 = only errors - * 2 = all function calls and errors (contains passwords too!) - * 3 = log line numbers in sql statements - */ - const LOG_LEVEL = 1; - - /** - * We store the content in the DB (no versioning) - */ - const STORE2DB = 1; - /** - * We store the content in the filesystem (egw_info/server/files_dir) (no versioning) - */ - const STORE2FS = 2; - /** - * default for operation, change that if you want to test with STORE2DB atm - */ - const DEFAULT_OPERATION = self::STORE2FS; - - /** - * operation mode of the opened file - * - * @var int - */ - protected $operation = self::DEFAULT_OPERATION; - - /** - * optional context param when opening the stream, null if no context passed - * - * @var mixed - */ - var $context; - - /** - * Path off the file opened by stream_open - * - * @var string - */ - protected $opened_path; - /** - * Mode of the file opened by stream_open - * - * @var int - */ - protected $opened_mode; - /** - * Stream of the opened file, either from the DB via PDO or the filesystem - * - * @var resource - */ - protected $opened_stream; - /** - * fs_id of opened file - * - * @var int - */ - protected $opened_fs_id; - /** - * Cache containing stat-infos from previous url_stat calls AND dir_opendir calls - * - * It's values are the columns read from the DB (fs_*), not the ones returned by url_stat! - * - * @var array $path => info-array pairs - */ - static protected $stat_cache = array(); - /** - * Reference to the PDO object we use - * - * @var PDO - */ - static protected $pdo; - /** - * Array with filenames of dir opened with dir_opendir - * - * @var array - */ - protected $opened_dir; - - /** - * Extra columns added since the intitial introduction of sqlfs - * - * Can be set to empty, so get queries running on old versions of sqlfs, eg. for schema updates - * - * @var string; - */ - static public $extra_columns = ',fs_link'; - - /** - * Clears our stat-cache - * - * Normaly not necessary, as it is automatically cleared/updated, UNLESS egw_vfs::$user changes! - * - * @param string $path ='/' - */ - public static function clearstatcache($path='/') - { - //error_log(__METHOD__."('$path')"); - unset($path); // not used - - self::$stat_cache = array(); - - egw_cache::setSession(self::EACL_APPNAME, 'extended_acl', self::$extended_acl = null); - } - - /** - * This method is called immediately after your stream object is created. - * - * @param string $url URL that was passed to fopen() and that this object is expected to retrieve - * @param string $mode mode used to open the file, as detailed for fopen() - * @param int $options additional flags set by the streams API (or'ed together): - * - STREAM_USE_PATH If path is relative, search for the resource using the include_path. - * - STREAM_REPORT_ERRORS If this flag is set, you are responsible for raising errors using trigger_error() during opening of the stream. - * If this flag is not set, you should not raise any errors. - * @param string &$opened_path full path of the file/resource, if the open was successfull and STREAM_USE_PATH was set - * @param array $overwrite_new =null if set create new file with values overwriten by the given ones - * @return boolean true if the ressource was opened successful, otherwise false - */ - function stream_open ( $url, $mode, $options, &$opened_path, array $overwrite_new=null ) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url,$mode,$options)"); - - $path = egw_vfs::parse_url($url,PHP_URL_PATH); - $this->operation = self::url2operation($url); - $dir = egw_vfs::dirname($url); - - $this->opened_path = $opened_path = $path; - $this->opened_mode = $mode = str_replace('b','',$mode); // we are always binary, like every Linux system - $this->opened_stream = null; - - if (!is_null($overwrite_new) || !($stat = static::url_stat($path,STREAM_URL_STAT_QUIET)) || $mode[0] == 'x') // file not found or file should NOT exist - { - if ($mode[0] == 'r' || // does $mode require the file to exist (r,r+) - $mode[0] == 'x' && $stat || // or file should not exist, but does - !($dir_stat=static::url_stat($dir,STREAM_URL_STAT_QUIET)) || // or parent dir does not exist create it - !egw_vfs::check_access($dir,egw_vfs::WRITABLE,$dir_stat)) // or we are not allowed to create it - { - self::_remove_password($url); - if (self::LOG_LEVEL) error_log(__METHOD__."($url,$mode,$options) file does not exist or can not be created!"); - if (($options & STREAM_REPORT_ERRORS)) - { - trigger_error(__METHOD__."($url,$mode,$options) file does not exist or can not be created!",E_USER_WARNING); - } - $this->opened_stream = $this->opened_path = $this->opened_mode = null; - return false; - } - // new file --> create it in the DB - $new_file = true; - $query = 'INSERT INTO '.self::TABLE.' (fs_name,fs_dir,fs_mode,fs_uid,fs_gid,fs_created,fs_modified,fs_creator,fs_mime,fs_size,fs_active'. - ') VALUES (:fs_name,:fs_dir,:fs_mode,:fs_uid,:fs_gid,:fs_created,:fs_modified,:fs_creator,:fs_mime,:fs_size,:fs_active)'; - if (self::LOG_LEVEL > 2) $query = '/* '.__METHOD__.': '.__LINE__.' */ '.$query; - $stmt = self::$pdo->prepare($query); - $values = array( - 'fs_name' => egw_vfs::basename($path), - 'fs_dir' => $dir_stat['ino'], - // we use the mode of the dir, so files in group dirs stay accessible by all members - 'fs_mode' => $dir_stat['mode'] & 0666, - // for the uid we use the uid of the dir if not 0=root or the current user otherwise - 'fs_uid' => $dir_stat['uid'] ? $dir_stat['uid'] : egw_vfs::$user, - // we allways use the group of the dir - 'fs_gid' => $dir_stat['gid'], - 'fs_created' => self::_pdo_timestamp(time()), - 'fs_modified' => self::_pdo_timestamp(time()), - 'fs_creator' => egw_vfs::$user, - 'fs_mime' => 'application/octet-stream', // required NOT NULL! - 'fs_size' => 0, - 'fs_active' => self::_pdo_boolean(true), - ); - if ($overwrite_new) $values = array_merge($values,$overwrite_new); - if (!$stmt->execute($values) || !($this->opened_fs_id = self::$pdo->lastInsertId('egw_sqlfs_fs_id_seq'))) - { - $this->opened_stream = $this->opened_path = $this->opened_mode = null; - if (self::LOG_LEVEL) error_log(__METHOD__."($url,$mode,$options) execute() failed: ".self::$pdo->errorInfo()); - return false; - } - if ($this->operation == self::STORE2DB) - { - // we buffer all write operations in a temporary file, which get's written on close - $this->opened_stream = tmpfile(); - } - // create the hash-dirs, if they not yet exist - elseif(!file_exists($fs_dir=egw_vfs::dirname(self::_fs_path($this->opened_fs_id)))) - { - $umaskbefore = umask(); - if (self::LOG_LEVEL > 1) error_log(__METHOD__." about to call mkdir for $fs_dir # Present UMASK:".decoct($umaskbefore)." called from:".function_backtrace()); - self::mkdir_recursive($fs_dir,0700,true); - } - } - // check if opend file is a directory - elseif($stat && ($stat['mode'] & self::MODE_DIR) == self::MODE_DIR) - { - if (self::LOG_LEVEL) error_log(__METHOD__."($url,$mode,$options) Is a directory!"); - if (($options & STREAM_REPORT_ERRORS)) - { - trigger_error(__METHOD__."($url,$mode,$options) Is a directory!",E_USER_WARNING); - } - $this->opened_stream = $this->opened_path = $this->opened_mode = null; - return false; - } - else - { - if ($mode == 'r' && !egw_vfs::check_access($url,egw_vfs::READABLE ,$stat) ||// we are not allowed to read - $mode != 'r' && !egw_vfs::check_access($url,egw_vfs::WRITABLE,$stat)) // or edit it - { - self::_remove_password($url); - $op = $mode == 'r' ? 'read' : 'edited'; - if (self::LOG_LEVEL) error_log(__METHOD__."($url,$mode,$options) file can not be $op!"); - if (($options & STREAM_REPORT_ERRORS)) - { - trigger_error(__METHOD__."($url,$mode,$options) file can not be $op!",E_USER_WARNING); - } - $this->opened_stream = $this->opened_path = $this->opened_mode = null; - return false; - } - $this->opened_fs_id = $stat['ino']; - - if ($this->operation == self::STORE2DB) - { - $stmt = self::$pdo->prepare($sql='SELECT fs_content FROM '.self::TABLE.' WHERE fs_id=?'); - $stmt->execute(array($stat['ino'])); - $stmt->bindColumn(1,$this->opened_stream,PDO::PARAM_LOB); - $stmt->fetch(PDO::FETCH_BOUND); - // hack to work around a current php bug (http://bugs.php.net/bug.php?id=40913) - // PDOStatement::bindColumn(,,PDO::PARAM_LOB) is not working for MySQL, content is returned as string :-( - if (is_string($this->opened_stream)) - { - $name = md5($url); - $GLOBALS[$name] =& $this->opened_stream; unset($this->opened_stream); - require_once(EGW_API_INC.'/class.global_stream_wrapper.inc.php'); - $this->opened_stream = fopen('global://'.$name,'r'); - unset($GLOBALS[$name]); // unset it, so it does not use up memory, once the stream is closed - } - //echo 'gettype($this->opened_stream)='; var_dump($this->opened_stream); - } - } - // do we operate directly on the filesystem --> open file from there - if ($this->operation == self::STORE2FS) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__." fopen (may create a directory? mkdir) ($this->opened_fs_id,$mode,$options)"); - if (!($this->opened_stream = fopen(self::_fs_path($this->opened_fs_id),$mode)) && $new_file) - { - // delete db entry again, if we are not able to open a new(!) file - unset($stmt); - $stmt = self::$pdo->prepare('DELETE FROM '.self::TABLE.' WHERE fs_id=:fs_id'); - $stmt->execute(array('fs_id' => $this->opened_fs_id)); - } - } - if ($mode[0] == 'a') // append modes: a, a+ - { - $this->stream_seek(0,SEEK_END); - } - if (!is_resource($this->opened_stream)) error_log(__METHOD__."($url,$mode,$options) NO stream, returning false!"); - - return is_resource($this->opened_stream); - } - - /** - * This method is called when the stream is closed, using fclose(). - * - * You must release any resources that were locked or allocated by the stream. - */ - function stream_close ( ) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__."()"); - - if (is_null($this->opened_path) || !is_resource($this->opened_stream) || !$this->opened_fs_id) - { - return false; - } - - if ($this->opened_mode != 'r') - { - $this->stream_seek(0,SEEK_END); - - // we need to update the mime-type, size and content (if STORE2DB) - $values = array( - 'fs_size' => $this->stream_tell(), - // todo: analyse the file for the mime-type - 'fs_mime' => mime_magic::filename2mime($this->opened_path), - 'fs_id' => $this->opened_fs_id, - 'fs_modifier' => egw_vfs::$user, - 'fs_modified' => self::_pdo_timestamp(time()), - ); - - if ($this->operation == self::STORE2FS) - { - $stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_size=:fs_size,fs_mime=:fs_mime,fs_modifier=:fs_modifier,fs_modified=:fs_modified WHERE fs_id=:fs_id'); - if (!($ret = $stmt->execute($values))) - { - error_log(__METHOD__."() execute() failed! errorInfo()=".array2string(self::$pdo->errorInfo())); - } - } - else - { - $stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_size=:fs_size,fs_mime=:fs_mime,fs_modifier=:fs_modifier,fs_modified=:fs_modified,fs_content=:fs_content WHERE fs_id=:fs_id'); - $this->stream_seek(0,SEEK_SET); // rewind to the start - foreach($values as $name => &$value) - { - $stmt->bindParam($name,$value); - } - $stmt->bindParam('fs_content', $this->opened_stream, PDO::PARAM_LOB); - if (!($ret = $stmt->execute())) - { - error_log(__METHOD__."() execute() failed! errorInfo()=".array2string(self::$pdo->errorInfo())); - } - } - } - else - { - $ret = true; - } - $ret = fclose($this->opened_stream) && $ret; - - unset(self::$stat_cache[$this->opened_path]); - $this->opened_stream = $this->opened_path = $this->opened_mode = $this->opend_fs_id = null; - $this->operation = self::DEFAULT_OPERATION; - - return $ret; - } - - /** - * This method is called in response to fread() and fgets() calls on the stream. - * - * You must return up-to count bytes of data from the current read/write position as a string. - * If there are less than count bytes available, return as many as are available. - * If no more data is available, return either FALSE or an empty string. - * You must also update the read/write position of the stream by the number of bytes that were successfully read. - * - * @param int $count - * @return string/false up to count bytes read or false on EOF - */ - function stream_read ( $count ) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__."($count) pos=$this->opened_pos"); - - if (is_resource($this->opened_stream)) - { - return fread($this->opened_stream,$count); - } - return false; - } - - /** - * This method is called in response to fwrite() calls on the stream. - * - * You should store data into the underlying storage used by your stream. - * If there is not enough room, try to store as many bytes as possible. - * You should return the number of bytes that were successfully stored in the stream, or 0 if none could be stored. - * You must also update the read/write position of the stream by the number of bytes that were successfully written. - * - * @param string $data - * @return integer - */ - function stream_write ( $data ) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__."($data)"); - - if (is_resource($this->opened_stream)) - { - return fwrite($this->opened_stream,$data); - } - return false; - } - - /** - * This method is called in response to feof() calls on the stream. - * - * Important: PHP 5.0 introduced a bug that wasn't fixed until 5.1: the return value has to be the oposite! - * - * if(version_compare(PHP_VERSION,'5.0','>=') && version_compare(PHP_VERSION,'5.1','<')) - * { - * $eof = !$eof; - * } - * - * @return boolean true if the read/write position is at the end of the stream and no more data availible, false otherwise - */ - function stream_eof ( ) - { - if (is_resource($this->opened_stream)) - { - return feof($this->opened_stream); - } - return false; - } - - /** - * This method is called in response to ftell() calls on the stream. - * - * @return integer current read/write position of the stream - */ - function stream_tell ( ) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__."()"); - - if (is_resource($this->opened_stream)) - { - return ftell($this->opened_stream); - } - return false; - } - - /** - * This method is called in response to fseek() calls on the stream. - * - * You should update the read/write position of the stream according to offset and whence. - * See fseek() for more information about these parameters. - * - * @param integer $offset - * @param integer $whence SEEK_SET - 0 - Set position equal to offset bytes - * SEEK_CUR - 1 - Set position to current location plus offset. - * SEEK_END - 2 - Set position to end-of-file plus offset. (To move to a position before the end-of-file, you need to pass a negative value in offset.) - * @return boolean TRUE if the position was updated, FALSE otherwise. - */ - function stream_seek ( $offset, $whence ) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__."($offset,$whence)"); - - if (is_resource($this->opened_stream)) - { - return !fseek($this->opened_stream,$offset,$whence); // fseek returns 0 on success and -1 on failure - } - return false; - } - - /** - * This method is called in response to fflush() calls on the stream. - * - * If you have cached data in your stream but not yet stored it into the underlying storage, you should do so now. - * - * @return booelan TRUE if the cached data was successfully stored (or if there was no data to store), or FALSE if the data could not be stored. - */ - function stream_flush ( ) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__."()"); - - if (is_resource($this->opened_stream)) - { - return fflush($this->opened_stream); - } - return false; - } - - /** - * This method is called in response to fstat() calls on the stream. - * - * If you plan to use your wrapper in a require_once you need to define stream_stat(). - * If you plan to allow any other tests like is_file()/is_dir(), you have to define url_stat(). - * stream_stat() must define the size of the file, or it will never be included. - * url_stat() must define mode, or is_file()/is_dir()/is_executable(), and any of those functions affected by clearstatcache() simply won't work. - * It's not documented, but directories must be a mode like 040777 (octal), and files a mode like 0100666. - * If you wish the file to be executable, use 7s instead of 6s. - * The last 3 digits are exactly the same thing as what you pass to chmod. - * 040000 defines a directory, and 0100000 defines a file. - * - * @return array containing the same values as appropriate for the stream. - */ - function stream_stat ( ) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__."($this->opened_path)"); - - return $this->url_stat($this->opened_path,0); - } - - /** - * This method is called in response to unlink() calls on URL paths associated with the wrapper. - * - * It should attempt to delete the item specified by path. - * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support unlinking! - * - * @param string $url - * @return boolean TRUE on success or FALSE on failure - */ - static function unlink ( $url, $parent_stat=null ) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url)"); - - $path = egw_vfs::parse_url($url,PHP_URL_PATH); - - // need to get parent stat from Sqlfs, not Vfs - if (!isset($parent_stat)) $parent_stat = static::url_stat(egw_vfs::dirname($path), STREAM_URL_STAT_LINK); - - if (!$parent_stat || !($stat = self::url_stat($path,STREAM_URL_STAT_LINK)) || - !egw_vfs::check_access(egw_vfs::dirname($path), egw_vfs::WRITABLE, $parent_stat)) - { - self::_remove_password($url); - if (self::LOG_LEVEL) error_log(__METHOD__."($url) permission denied!"); - return false; // no permission or file does not exist - } - if ($stat['mime'] == self::DIR_MIME_TYPE) - { - self::_remove_password($url); - if (self::LOG_LEVEL) error_log(__METHOD__."($url) is NO file!"); - return false; // no permission or file does not exist - } - $stmt = self::$pdo->prepare('DELETE FROM '.self::TABLE.' WHERE fs_id=:fs_id'); - unset(self::$stat_cache[$path]); - - if (($ret = $stmt->execute(array('fs_id' => $stat['ino'])))) - { - if (self::url2operation($url) == self::STORE2FS && - ($stat['mode'] & self::MODE_LINK) != self::MODE_LINK) - { - unlink(self::_fs_path($stat['ino'])); - } - // delete props - unset($stmt); - $stmt = self::$pdo->prepare('DELETE FROM '.self::PROPS_TABLE.' WHERE fs_id=?'); - $stmt->execute(array($stat['ino'])); - } - return $ret; - } - - /** - * This method is called in response to rename() calls on URL paths associated with the wrapper. - * - * It should attempt to rename the item specified by path_from to the specification given by path_to. - * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support renaming. - * - * The regular filesystem stream-wrapper returns an error, if $url_from and $url_to are not either both files or both dirs! - * - * @param string $url_from - * @param string $url_to - * @return boolean TRUE on success or FALSE on failure - */ - static function rename ( $url_from, $url_to) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url_from,$url_to)"); - - $path_from = egw_vfs::parse_url($url_from,PHP_URL_PATH); - $from_dir = egw_vfs::dirname($path_from); - $path_to = egw_vfs::parse_url($url_to,PHP_URL_PATH); - $to_dir = egw_vfs::dirname($path_to); - $operation = self::url2operation($url_from); - - // we have to use array($class,'url_stat'), as $class.'::url_stat' requires PHP 5.2.3 and we currently only require 5.2+ - if (!($from_stat = static::url_stat($path_from, 0)) || - !egw_vfs::check_access($from_dir, egw_vfs::WRITABLE, $from_dir_stat = static::url_stat($from_dir, 0))) - { - self::_remove_password($url_from); - self::_remove_password($url_to); - if (self::LOG_LEVEL) error_log(__METHOD__."($url_from,$url_to): $path_from permission denied!"); - return false; // no permission or file does not exist - } - if (!egw_vfs::check_access($to_dir, egw_vfs::WRITABLE, $to_dir_stat = static::url_stat($to_dir, 0))) - { - self::_remove_password($url_from); - self::_remove_password($url_to); - if (self::LOG_LEVEL) error_log(__METHOD__."($url_from,$url_to): $path_to permission denied!"); - return false; // no permission or parent-dir does not exist - } - // the filesystem stream-wrapper does NOT allow to rename files to directories, as this makes problems - // for our vfs too, we abort here with an error, like the filesystem one does - if (($to_stat = static::url_stat($path_to, 0)) && - ($to_stat['mime'] === self::DIR_MIME_TYPE) !== ($from_stat['mime'] === self::DIR_MIME_TYPE)) - { - self::_remove_password($url_from); - self::_remove_password($url_to); - $is_dir = $to_stat['mime'] === self::DIR_MIME_TYPE ? 'a' : 'no'; - if (self::LOG_LEVEL) error_log(__METHOD__."($url_to,$url_from) $path_to is $is_dir directory!"); - return false; // no permission or file does not exist - } - // if destination file already exists, delete it - if ($to_stat && !static::unlink($url_to,$operation)) - { - self::_remove_password($url_to); - if (self::LOG_LEVEL) error_log(__METHOD__."($url_to,$url_from) can't unlink existing $url_to!"); - return false; - } - unset(self::$stat_cache[$path_from]); - unset(self::$stat_cache[$path_to]); - - $stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_dir=:fs_dir,fs_name=:fs_name WHERE fs_dir=:old_dir AND fs_name=:old_name'); - $ok = $stmt->execute(array( - 'fs_dir' => $to_dir_stat['ino'], - 'fs_name' => egw_vfs::basename($path_to), - 'old_dir' => $from_dir_stat['ino'], - 'old_name' => $from_stat['name'], - )); - unset($stmt); - - // check if extension changed and update mime-type in that case (as we currently determine mime-type by it's extension!) - // fixes eg. problems with MsWord storing file with .tmp extension and then renaming to .doc - if ($ok && ($new_mime = egw_vfs::mime_content_type($url_to,true)) != egw_vfs::mime_content_type($url_to)) - { - //echo "

egw_vfs::nime_content_type($url_to,true) = $new_mime

\n"; - $stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_mime=:fs_mime WHERE fs_id=:fs_id'); - $stmt->execute(array( - 'fs_mime' => $new_mime, - 'fs_id' => $from_stat['ino'], - )); - unset(self::$stat_cache[$path_to]); - } - return $ok; - } - - /** - * due to problems with recursive directory creation, we have our own here - */ - protected static function mkdir_recursive($pathname, $mode, $depth=0) - { - $maxdepth=10; - $depth2propagate = (int)$depth + 1; - if ($depth2propagate > $maxdepth) return is_dir($pathname); - is_dir(egw_vfs::dirname($pathname)) || self::mkdir_recursive(egw_vfs::dirname($pathname), $mode, $depth2propagate); - return is_dir($pathname) || @mkdir($pathname, $mode); - } - - /** - * This method is called in response to mkdir() calls on URL paths associated with the wrapper. - * - * It should attempt to create the directory specified by path. - * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support creating directories. - * - * @param string $url - * @param int $mode - * @param int $options Posible values include STREAM_REPORT_ERRORS and STREAM_MKDIR_RECURSIVE - * @return boolean TRUE on success or FALSE on failure - */ - static function mkdir ( $url, $mode, $options ) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url,$mode,$options)"); - if (self::LOG_LEVEL > 1) error_log(__METHOD__." called from:".function_backtrace()); - $path = egw_vfs::parse_url($url,PHP_URL_PATH); - - if (self::url_stat($path,STREAM_URL_STAT_QUIET)) - { - self::_remove_password($url); - if (self::LOG_LEVEL) error_log(__METHOD__."('$url',$mode,$options) already exist!"); - if (!($options & STREAM_REPORT_ERRORS)) - { - //throw new Exception(__METHOD__."('$url',$mode,$options) already exist!"); - trigger_error(__METHOD__."('$url',$mode,$options) already exist!",E_USER_WARNING); - } - return false; - } - $parent_path = egw_vfs::dirname($path); - if (($query = egw_vfs::parse_url($url,PHP_URL_QUERY))) $parent_path .= '?'.$query; - $parent = self::url_stat($parent_path,STREAM_URL_STAT_QUIET); - - // check if we should also create all non-existing path components and our parent does not exist, - // if yes call ourself recursive with the parent directory - if (($options & STREAM_MKDIR_RECURSIVE) && $parent_path != '/' && !$parent) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__." creating parents: $parent_path, $mode"); - if (!self::mkdir($parent_path,$mode,$options)) - { - return false; - } - $parent = self::url_stat($parent_path,0); - } - if (!$parent || !egw_vfs::check_access($parent_path,egw_vfs::WRITABLE,$parent)) - { - self::_remove_password($url); - if (self::LOG_LEVEL) error_log(__METHOD__."('$url',$mode,$options) permission denied!"); - if (!($options & STREAM_REPORT_ERRORS)) - { - trigger_error(__METHOD__."('$url',$mode,$options) permission denied!",E_USER_WARNING); - } - return false; // no permission or file does not exist - } - unset(self::$stat_cache[$path]); - $stmt = self::$pdo->prepare('INSERT INTO '.self::TABLE.' (fs_name,fs_dir,fs_mode,fs_uid,fs_gid,fs_size,fs_mime,fs_created,fs_modified,fs_creator'. - ') VALUES (:fs_name,:fs_dir,:fs_mode,:fs_uid,:fs_gid,:fs_size,:fs_mime,:fs_created,:fs_modified,:fs_creator)'); - if (($ok = $stmt->execute(array( - 'fs_name' => egw_vfs::basename($path), - 'fs_dir' => $parent['ino'], - 'fs_mode' => $parent['mode'], - 'fs_uid' => $parent['uid'], - 'fs_gid' => $parent['gid'], - 'fs_size' => 0, - 'fs_mime' => self::DIR_MIME_TYPE, - 'fs_created' => self::_pdo_timestamp(time()), - 'fs_modified' => self::_pdo_timestamp(time()), - 'fs_creator' => egw_vfs::$user, - )))) - { - // check if some other process created the directory parallel to us (sqlfs would gives SQL errors later!) - $new_fs_id = self::$pdo->lastInsertId('egw_sqlfs_fs_id_seq'); - - unset($stmt); // free statement object, on some installs a new prepare fails otherwise! - - $stmt = self::$pdo->prepare($q='SELECT COUNT(*) FROM '.self::TABLE. - ' WHERE fs_dir=:fs_dir AND fs_active=:fs_active AND fs_name'.self::$case_sensitive_equal.':fs_name'); - if ($stmt->execute(array( - 'fs_dir' => $parent['ino'], - 'fs_active' => self::_pdo_boolean(true), - 'fs_name' => egw_vfs::basename($path), - )) && $stmt->fetchColumn() > 1) // if there's more then one --> remove our new dir - { - self::$pdo->query('DELETE FROM '.self::TABLE.' WHERE fs_id='.$new_fs_id); - } - } - return $ok; - } - - /** - * This method is called in response to rmdir() calls on URL paths associated with the wrapper. - * - * It should attempt to remove the directory specified by path. - * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support removing directories. - * - * @param string $url - * @param int $options Possible values include STREAM_REPORT_ERRORS. - * @return boolean TRUE on success or FALSE on failure. - */ - static function rmdir ( $url, $options ) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url)"); - - $path = egw_vfs::parse_url($url,PHP_URL_PATH); - $parent = egw_vfs::dirname($path); - - if (!($stat = self::url_stat($path,0)) || $stat['mime'] != self::DIR_MIME_TYPE || - !egw_vfs::check_access($parent, egw_vfs::WRITABLE, static::url_stat($parent,0))) - { - self::_remove_password($url); - $err_msg = __METHOD__."($url,$options) ".(!$stat ? 'not found!' : - ($stat['mime'] != self::DIR_MIME_TYPE ? 'not a directory!' : 'permission denied!')); - if (self::LOG_LEVEL) error_log($err_msg); - if (!($options & STREAM_REPORT_ERRORS)) - { - trigger_error($err_msg,E_USER_WARNING); - } - return false; // no permission or file does not exist - } - $stmt = self::$pdo->prepare('SELECT COUNT(*) FROM '.self::TABLE.' WHERE fs_dir=?'); - $stmt->execute(array($stat['ino'])); - if ($stmt->fetchColumn()) - { - self::_remove_password($url); - if (self::LOG_LEVEL) error_log(__METHOD__."($url,$options) dir is not empty!"); - if (!($options & STREAM_REPORT_ERRORS)) - { - trigger_error(__METHOD__."('$url',$options) dir is not empty!",E_USER_WARNING); - } - return false; - } - unset(self::$stat_cache[$path]); - unset($stmt); // free statement object, on some installs a new prepare fails otherwise! - - $del_stmt = self::$pdo->prepare('DELETE FROM '.self::TABLE.' WHERE fs_id=?'); - if (($ret = $del_stmt->execute(array($stat['ino'])))) - { - self::eacl($path,null,false,$stat['ino']); // remove all (=false) evtl. existing extended acl for that dir - // delete props - unset($del_stmt); - $del_stmt = self::$pdo->prepare('DELETE FROM '.self::PROPS_TABLE.' WHERE fs_id=?'); - $del_stmt->execute(array($stat['ino'])); - } - return $ret; - } - - /** - * This is not (yet) a stream-wrapper function, but it's necessary and can be used static - * - * @param string $url - * @param int $time =null modification time (unix timestamp), default null = current time - * @param int $atime =null access time (unix timestamp), default null = current time, not implemented in the vfs! - */ - static function touch($url,$time=null,$atime=null) - { - unset($atime); // not used - if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url, $time)"); - - $path = egw_vfs::parse_url($url,PHP_URL_PATH); - - if (!($stat = self::url_stat($path,STREAM_URL_STAT_QUIET))) - { - // file does not exist --> create an empty one - if (!($f = fopen(self::SCHEME.'://default'.$path,'w')) || !fclose($f)) - { - return false; - } - if (is_null($time)) - { - return true; // new (empty) file created with current mod time - } - $stat = self::url_stat($path,0); - } - unset(self::$stat_cache[$path]); - $stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_modified=:fs_modified,fs_modifier=:fs_modifier WHERE fs_id=:fs_id'); - - return $stmt->execute(array( - 'fs_modified' => self::_pdo_timestamp($time ? $time : time()), - 'fs_modifier' => egw_vfs::$user, - 'fs_id' => $stat['ino'], - )); - } - - /** - * Chown command, not yet a stream-wrapper function, but necessary - * - * @param string $url - * @param int $owner - * @return boolean - */ - static function chown($url,$owner) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url,$owner)"); - - $path = egw_vfs::parse_url($url,PHP_URL_PATH); - - if (!($stat = self::url_stat($path,0))) - { - if (self::LOG_LEVEL) error_log(__METHOD__."($url,$owner) no such file or directory!"); - trigger_error("No such file or directory $url !",E_USER_WARNING); - return false; - } - if (!egw_vfs::$is_root) - { - if (self::LOG_LEVEL) error_log(__METHOD__."($url,$owner) only root can do that!"); - trigger_error("Only root can do that!",E_USER_WARNING); - return false; - } - if ($owner < 0 || $owner && !$GLOBALS['egw']->accounts->id2name($owner)) // not a user (0 == root) - { - if (self::LOG_LEVEL) error_log(__METHOD__."($url,$owner) unknown (numeric) user id!"); - trigger_error(__METHOD__."($url,$owner) Unknown (numeric) user id!",E_USER_WARNING); - //throw new Exception(__METHOD__."($url,$owner) Unknown (numeric) user id!"); - return false; - } - $stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_uid=:fs_uid WHERE fs_id=:fs_id'); - - // update stat-cache - if ($path != '/' && substr($path,-1) == '/') $path = substr($path, 0, -1); - self::$stat_cache[$path]['fs_uid'] = $owner; - - return $stmt->execute(array( - 'fs_uid' => (int) $owner, - 'fs_id' => $stat['ino'], - )); - } - - /** - * Chgrp command, not yet a stream-wrapper function, but necessary - * - * @param string $url - * @param int $owner - * @return boolean - */ - static function chgrp($url,$owner) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url,$owner)"); - - $path = egw_vfs::parse_url($url,PHP_URL_PATH); - - if (!($stat = self::url_stat($path,0))) - { - if (self::LOG_LEVEL) error_log(__METHOD__."($url,$owner) no such file or directory!"); - trigger_error("No such file or directory $url !",E_USER_WARNING); - return false; - } - if (!egw_vfs::has_owner_rights($path,$stat)) - { - if (self::LOG_LEVEL) error_log(__METHOD__."($url,$owner) only owner or root can do that!"); - trigger_error("Only owner or root can do that!",E_USER_WARNING); - return false; - } - if ($owner < 0) $owner = -$owner; // sqlfs uses a positiv group id's! - - if ($owner && !$GLOBALS['egw']->accounts->id2name(-$owner)) // not a group - { - if (self::LOG_LEVEL) error_log(__METHOD__."($url,$owner) unknown (numeric) group id!"); - trigger_error("Unknown (numeric) group id!",E_USER_WARNING); - return false; - } - $stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_gid=:fs_gid WHERE fs_id=:fs_id'); - - // update stat-cache - if ($path != '/' && substr($path,-1) == '/') $path = substr($path, 0, -1); - self::$stat_cache[$path]['fs_gid'] = $owner; - - return $stmt->execute(array( - 'fs_gid' => $owner, - 'fs_id' => $stat['ino'], - )); - } - - /** - * Chmod command, not yet a stream-wrapper function, but necessary - * - * @param string $url - * @param int $mode - * @return boolean - */ - static function chmod($url,$mode) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url, $mode)"); - - $path = egw_vfs::parse_url($url,PHP_URL_PATH); - - if (!($stat = self::url_stat($path,0))) - { - if (self::LOG_LEVEL) error_log(__METHOD__."($url, $mode) no such file or directory!"); - trigger_error("No such file or directory $url !",E_USER_WARNING); - return false; - } - if (!egw_vfs::has_owner_rights($path,$stat)) - { - if (self::LOG_LEVEL) error_log(__METHOD__."($url, $mode) only owner or root can do that!"); - trigger_error("Only owner or root can do that!",E_USER_WARNING); - return false; - } - if (!is_numeric($mode)) // not a mode - { - if (self::LOG_LEVEL) error_log(__METHOD__."($url, $mode) no (numeric) mode!"); - trigger_error("No (numeric) mode!",E_USER_WARNING); - return false; - } - $stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_mode=:fs_mode WHERE fs_id=:fs_id'); - - // update stat cache - if ($path != '/' && substr($path,-1) == '/') $path = substr($path, 0, -1); - self::$stat_cache[$path]['fs_mode'] = ((int) $mode) & 0777; - - return $stmt->execute(array( - 'fs_mode' => ((int) $mode) & 0777, // we dont store the file and dir bits, give int overflow! - 'fs_id' => $stat['ino'], - )); - } - - - /** - * This method is called immediately when your stream object is created for examining directory contents with opendir(). - * - * @param string $url URL that was passed to opendir() and that this object is expected to explore. - * @param int $options - * @return booelan - */ - function dir_opendir ( $url, $options ) - { - $this->opened_dir = null; - - $path = egw_vfs::parse_url($url,PHP_URL_PATH); - - if (!($stat = self::url_stat($url,0)) || // dir not found - $stat['mime'] != self::DIR_MIME_TYPE || // no dir - !egw_vfs::check_access($url,egw_vfs::EXECUTABLE|egw_vfs::READABLE,$stat)) // no access - { - self::_remove_password($url); - $msg = $stat['mime'] != self::DIR_MIME_TYPE ? "$url is no directory" : 'permission denied'; - if (self::LOG_LEVEL) error_log(__METHOD__."('$url',$options) $msg!"); - $this->opened_dir = null; - return false; - } - $this->opened_dir = array(); - $query = 'SELECT fs_id,fs_name,fs_mode,fs_uid,fs_gid,fs_size,fs_mime,fs_created,fs_modified'.self::$extra_columns. - ' FROM '.self::TABLE.' WHERE fs_dir=? AND fs_active='.self::_pdo_boolean(true). - " ORDER BY fs_mime='httpd/unix-directory' DESC, fs_name ASC"; - //if (self::LOG_LEVEL > 2) $query = '/* '.__METHOD__.': '.__LINE__.' */ '.$query; - if (self::LOG_LEVEL > 2) $query = '/* '.__METHOD__."($url,$options)".' */ '.$query; - - $stmt = self::$pdo->prepare($query); - $stmt->setFetchMode(PDO::FETCH_ASSOC); - if ($stmt->execute(array($stat['ino']))) - { - foreach($stmt as $file) - { - $this->opened_dir[] = $file['fs_name']; - self::$stat_cache[egw_vfs::concat($path,$file['fs_name'])] = $file; - } - } - if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url,$options): ".implode(', ',$this->opened_dir)); - reset($this->opened_dir); - - return true; - } - - /** - * This method is called in response to stat() calls on the URL paths associated with the wrapper. - * - * It should return as many elements in common with the system function as possible. - * Unknown or unavailable values should be set to a rational value (usually 0). - * - * If you plan to use your wrapper in a require_once you need to define stream_stat(). - * If you plan to allow any other tests like is_file()/is_dir(), you have to define url_stat(). - * stream_stat() must define the size of the file, or it will never be included. - * url_stat() must define mode, or is_file()/is_dir()/is_executable(), and any of those functions affected by clearstatcache() simply won't work. - * It's not documented, but directories must be a mode like 040777 (octal), and files a mode like 0100666. - * If you wish the file to be executable, use 7s instead of 6s. - * The last 3 digits are exactly the same thing as what you pass to chmod. - * 040000 defines a directory, and 0100000 defines a file. - * - * @param string $url - * @param int $flags holds additional flags set by the streams API. It can hold one or more of the following values OR'd together: - * - STREAM_URL_STAT_LINK For resources with the ability to link to other resource (such as an HTTP Location: forward, - * or a filesystem symlink). This flag specified that only information about the link itself should be returned, - * not the resource pointed to by the link. - * This flag is set in response to calls to lstat(), is_link(), or filetype(). - * - STREAM_URL_STAT_QUIET If this flag is set, your wrapper should not raise any errors. If this flag is not set, - * you are responsible for reporting errors using the trigger_error() function during stating of the path. - * stat triggers it's own warning anyway, so it makes no sense to trigger one by our stream-wrapper! - * @param boolean $eacl_access =null allows extending classes to pass the value of their check_extended_acl() method (no lsb!) - * @return array - */ - static function url_stat ( $url, $flags, $eacl_access=null ) - { - static $max_subquery_depth=null; - if (is_null($max_subquery_depth)) - { - $max_subquery_depth = $GLOBALS['egw_info']['server']['max_subquery_depth']; - if (!$max_subquery_depth) $max_subquery_depth = 7; // setting current default of 7, if nothing set - } - if (self::LOG_LEVEL > 1) error_log(__METHOD__."('$url',$flags,$eacl_access)"); - - $path = egw_vfs::parse_url($url,PHP_URL_PATH); - - // webdav adds a trailing slash to dirs, which causes url_stat to NOT find the file otherwise - if ($path != '/' && substr($path,-1) == '/') - { - $path = substr($path,0,-1); - } - if (empty($path)) - { - return false; // is invalid and gives sql error - } - // check if we already have the info from the last dir_open call, as the old vfs reads it anyway from the db - if (self::$stat_cache && isset(self::$stat_cache[$path]) && (is_null($eacl_access) || self::$stat_cache[$path] !== false)) - { - return self::$stat_cache[$path] ? self::_vfsinfo2stat(self::$stat_cache[$path]) : false; - } - - if (!is_object(self::$pdo)) - { - self::_pdo(); - } - $base_query = 'SELECT fs_id,fs_name,fs_mode,fs_uid,fs_gid,fs_size,fs_mime,fs_created,fs_modified'.self::$extra_columns. - ' FROM '.self::TABLE.' WHERE fs_active='.self::_pdo_boolean(true). - ' AND fs_name'.self::$case_sensitive_equal.'? AND fs_dir='; - $parts = explode('/',$path); - - // if we have extendes acl access to the url, we dont need and can NOT include the sql for the readable check - if (is_null($eacl_access)) - { - $eacl_access = self::check_extended_acl($path,egw_vfs::READABLE); // should be static::check_extended_acl, but no lsb! - } - - try { - foreach($parts as $n => $name) - { - if ($n == 0) - { - $query = (int) ($path != '/'); // / always has fs_id == 1, no need to query it ($path=='/' needs fs_dir=0!) - } - elseif ($n < count($parts)-1) - { - // MySQL 5.0 has a nesting limit for subqueries - // --> we replace the so far cumulated subqueries with their result - // no idea about the other DBMS, but this does NOT hurt ... - // --> depth limit of subqueries is now dynamicly decremented in catch - if ($n > 1 && !(($n-1) % $max_subquery_depth) && !($query = self::$pdo->query($query)->fetchColumn())) - { - if (self::LOG_LEVEL > 1) - { - self::_remove_password($url); - error_log(__METHOD__."('$url',$flags) file or directory not found!"); - } - // we also store negatives (all methods creating new files/dirs have to unset the stat-cache!) - return self::$stat_cache[$path] = false; - } - $query = 'SELECT fs_id FROM '.self::TABLE.' WHERE fs_dir=('.$query.') AND fs_active='. - self::_pdo_boolean(true).' AND fs_name'.self::$case_sensitive_equal.self::$pdo->quote($name); - - // if we are not root AND have no extended acl access, we need to make sure the user has the right to tranverse all parent directories (read-rights) - if (!egw_vfs::$is_root && !$eacl_access) - { - if (!egw_vfs::$user) - { - self::_remove_password($url); - if (self::LOG_LEVEL > 1) error_log(__METHOD__."('$url',$flags) permission denied, no user-id and not root!"); - return false; - } - $query .= ' AND '.self::_sql_readable(); - } - } - else - { - $query = str_replace('fs_name'.self::$case_sensitive_equal.'?','fs_name'.self::$case_sensitive_equal.self::$pdo->quote($name),$base_query).'('.$query.')'; - } - } - if (self::LOG_LEVEL > 2) $query = '/* '.__METHOD__."($url,$flags,$eacl_access)".' */ '.$query; - //if (self::LOG_LEVEL > 2) $query = '/* '.__METHOD__.': '.__LINE__.' */ '.$query; - - if (!($result = self::$pdo->query($query)) || !($info = $result->fetch(PDO::FETCH_ASSOC))) - { - if (self::LOG_LEVEL > 1) - { - self::_remove_password($url); - error_log(__METHOD__."('$url',$flags) file or directory not found!"); - } - // we also store negatives (all methods creating new files/dirs have to unset the stat-cache!) - return self::$stat_cache[$path] = false; - } - } - catch (PDOException $e) { - // decrement subquery limit by 1 and try again, if not already smaller then 3 - if ($max_subquery_depth < 3) - { - throw new egw_exception_db($e->getMessage()); - } - $GLOBALS['egw_info']['server']['max_subquery_depth'] = --$max_subquery_depth; - error_log(__METHOD__."() decremented max_subquery_depth to $max_subquery_depth"); - config::save_value('max_subquery_depth', $max_subquery_depth, 'phpgwapi'); - if (method_exists($GLOBALS['egw'],'invalidate_session_cache')) $GLOBALS['egw']->invalidate_session_cache(); - return self::url_stat($url, $flags, $eacl_access); - } - self::$stat_cache[$path] = $info; - - if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url,$flags)=".array2string($info)); - return self::_vfsinfo2stat($info); - } - - /** - * Return readable check as sql (to be AND'ed into the query), only use if !egw_vfs::$is_root - * - * @return string - */ - protected function _sql_readable() - { - static $sql_read_acl=null; - - if (is_null($sql_read_acl)) - { - foreach($GLOBALS['egw']->accounts->memberships(egw_vfs::$user,true) as $gid) - { - $memberships[] = abs($gid); // sqlfs stores the gid's positiv - } - // using octal numbers with mysql leads to funny results (select 384 & 0400 --> 384 not 256=0400) - // 256 = 0400, 32 = 040 - $sql_read_acl = '((fs_mode & 4)=4 OR (fs_mode & 256)=256 AND fs_uid='.(int)egw_vfs::$user. - ($memberships ? ' OR (fs_mode & 32)=32 AND fs_gid IN('.implode(',',$memberships).')' : '').')'; - //error_log(__METHOD__."() egw_vfs::\$user=".array2string(egw_vfs::$user).' --> memberships='.array2string($memberships).' --> '.$sql_read_acl.($memberships?'':': '.function_backtrace())); - } - return $sql_read_acl; - } - - /** - * This method is called in response to readdir(). - * - * It should return a string representing the next filename in the location opened by dir_opendir(). - * - * @return string - */ - function dir_readdir ( ) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__."( )"); - - if (!is_array($this->opened_dir)) return false; - - $file = current($this->opened_dir); next($this->opened_dir); - - return $file; - } - - /** - * This method is called in response to rewinddir(). - * - * It should reset the output generated by dir_readdir(). i.e.: - * The next call to dir_readdir() should return the first entry in the location returned by dir_opendir(). - * - * @return boolean - */ - function dir_rewinddir ( ) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__."( )"); - - if (!is_array($this->opened_dir)) return false; - - reset($this->opened_dir); - - return true; - } - - /** - * This method is called in response to closedir(). - * - * You should release any resources which were locked or allocated during the opening and use of the directory stream. - * - * @return boolean - */ - function dir_closedir ( ) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__."( )"); - - if (!is_array($this->opened_dir)) return false; - - $this->opened_dir = null; - - return true; - } - - /** - * This method is called in response to readlink(). - * - * The readlink value is read by url_stat or dir_opendir and therefore cached in the stat-cache. - * - * @param string $path - * @return string|boolean content of the symlink or false if $url is no symlink (or not found) - */ - static function readlink($path) - { - $link = !($lstat = self::url_stat($path,STREAM_URL_STAT_LINK)) || is_null($lstat['readlink']) ? false : $lstat['readlink']; - - if (self::LOG_LEVEL > 1) error_log(__METHOD__."('$path') = $link"); - - return $link; - } - - /** - * Method called for symlink() - * - * @param string $target - * @param string $link - * @return boolean true on success false on error - */ - static function symlink($target,$link) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__."('$target','$link')"); - - if (self::url_stat($link,0) || !($dir = egw_vfs::dirname($link)) || - !egw_vfs::check_access($dir,egw_vfs::WRITABLE,$dir_stat=self::url_stat($dir,0))) - { - if (self::LOG_LEVEL > 0) error_log(__METHOD__."('$target','$link') returning false! (!stat('$link') || !is_writable('$dir'))"); - return false; // $link already exists or parent dir does not - } - $query = 'INSERT INTO '.self::TABLE.' (fs_name,fs_dir,fs_mode,fs_uid,fs_gid,fs_created,fs_modified,fs_creator,fs_mime,fs_size,fs_link'. - ') VALUES (:fs_name,:fs_dir,:fs_mode,:fs_uid,:fs_gid,:fs_created,:fs_modified,:fs_creator,:fs_mime,:fs_size,:fs_link)'; - if (self::LOG_LEVEL > 2) $query = '/* '.__METHOD__.': '.__LINE__.' */ '.$query; - $stmt = self::$pdo->prepare($query); - unset(self::$stat_cache[egw_vfs::parse_url($link,PHP_URL_PATH)]); - - return !!$stmt->execute(array( - 'fs_name' => egw_vfs::basename($link), - 'fs_dir' => $dir_stat['ino'], - 'fs_mode' => ($dir_stat['mode'] & 0666), - 'fs_uid' => $dir_stat['uid'] ? $dir_stat['uid'] : egw_vfs::$user, - 'fs_gid' => $dir_stat['gid'], - 'fs_created' => self::_pdo_timestamp(time()), - 'fs_modified' => self::_pdo_timestamp(time()), - 'fs_creator' => egw_vfs::$user, - 'fs_mime' => self::SYMLINK_MIME_TYPE, - 'fs_size' => bytes($target), - 'fs_link' => $target, - )); - } - - private static $extended_acl; - - /** - * Check if extendes ACL (stored in eGW's ACL table) grants access - * - * The extended ACL is inherited, so it's valid for all subdirs and the included files! - * The used algorithm break on the first match. It could be used, to disallow further access. - * - * @param string $url url to check - * @param int $check mode to check: one or more or'ed together of: 4 = read, 2 = write, 1 = executable - * @return boolean - */ - static function check_extended_acl($url,$check) - { - $url_path = egw_vfs::parse_url($url,PHP_URL_PATH); - - if (is_null(self::$extended_acl)) - { - self::_read_extended_acl(); - } - $access = false; - foreach(self::$extended_acl as $path => $rights) - { - if ($path == $url_path || substr($url_path,0,strlen($path)+1) == $path.'/') - { - $access = ($rights & $check) == $check; - break; - } - } - if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url,$check) ".($access?"access granted by $path=$rights":'no access!!!')); - return $access; - } - - /** - * Read the extended acl via acl::get_grants('sqlfs') - * - */ - static protected function _read_extended_acl() - { - if ((self::$extended_acl = egw_cache::getSession(self::EACL_APPNAME, 'extended_acl'))) - { - return; // ext. ACL read from session. - } - self::$extended_acl = array(); - if (($rights = $GLOBALS['egw']->acl->get_all_location_rights(egw_vfs::$user,self::EACL_APPNAME))) - { - $pathes = self::id2path(array_keys($rights)); - } - foreach($rights as $fs_id => $right) - { - $path = $pathes[$fs_id]; - if (isset($path)) - { - self::$extended_acl[$path] = (int)$right; - } - } - // sort by length descending, to allow more specific pathes to have precedence - uksort(self::$extended_acl, function($a,$b) { - return strlen($b)-strlen($a); - }); - egw_cache::setSession(self::EACL_APPNAME, 'extended_acl', self::$extended_acl); - if (self::LOG_LEVEL > 1) error_log(__METHOD__.'() '.array2string(self::$extended_acl)); - } - - /** - * Appname used with the acl class to store the extended acl - */ - const EACL_APPNAME = 'sqlfs'; - - /** - * Set or delete extended acl for a given path and owner (or delete them if is_null($rights) - * - * Only root, the owner of the path or an eGW admin (only if there's no owner but a group) are allowd to set eACL's! - * - * @param string $path string with path - * @param int $rights =null rights to set, or null to delete the entry - * @param int|boolean $owner =null owner for whom to set the rights, null for the current user, or false to delete all rights for $path - * @param int $fs_id =null fs_id to use, to not query it again (eg. because it's already deleted) - * @return boolean true if acl is set/deleted, false on error - */ - static function eacl($path,$rights=null,$owner=null,$fs_id=null) - { - if ($path[0] != '/') - { - $path = egw_vfs::parse_url($path,PHP_URL_PATH); - } - if (is_null($fs_id)) - { - if (!($stat = self::url_stat($path,0))) - { - if (self::LOG_LEVEL) error_log(__METHOD__."($path,$rights,$owner,$fs_id) no such file or directory!"); - return false; // $path not found - } - if (!egw_vfs::has_owner_rights($path,$stat)) // not group dir and user is eGW admin - { - if (self::LOG_LEVEL) error_log(__METHOD__."($path,$rights,$owner,$fs_id) permission denied!"); - return false; // permission denied - } - $fs_id = $stat['ino']; - } - if (is_null($owner)) - { - $owner = egw_vfs::$user; - } - if (is_null($rights) || $owner === false) - { - // delete eacl - if (is_null($owner) || $owner == egw_vfs::$user || - $owner < 0 && egw_vfs::$user && in_array($owner,$GLOBALS['egw']->accounts->memberships(egw_vfs::$user,true))) - { - self::$extended_acl = null; // force new read of eACL, as there could be multiple eACL for that path - } - $ret = $GLOBALS['egw']->acl->delete_repository(self::EACL_APPNAME,$fs_id,(int)$owner); - } - else - { - if (isset(self::$extended_acl) && ($owner == egw_vfs::$user || - $owner < 0 && egw_vfs::$user && in_array($owner,$GLOBALS['egw']->accounts->memberships(egw_vfs::$user,true)))) - { - // set rights for this class, if applicable - self::$extended_acl[$path] |= $rights; - } - $ret = $GLOBALS['egw']->acl->add_repository(self::EACL_APPNAME,$fs_id,$owner,$rights); - } - if ($ret) - { - egw_cache::setSession(self::EACL_APPNAME, 'extended_acl', self::$extended_acl); - } - if (self::LOG_LEVEL > 1) error_log(__METHOD__."($path,$rights,$owner,$fs_id)=".(int)$ret); - return $ret; - } - - /** - * Get all ext. ACL set for a path - * - * Calls itself recursive, to get the parent directories - * - * @param string $path - * @return array|boolean array with array('path'=>$path,'owner'=>$owner,'rights'=>$rights) or false if $path not found - */ - function get_eacl($path) - { - if (!($stat = static::url_stat($path, STREAM_URL_STAT_QUIET))) - { - error_log(__METHOD__.__LINE__.' '.array2string($path).' not found!'); - return false; // not found - } - $eacls = array(); - foreach($GLOBALS['egw']->acl->get_all_rights($stat['ino'],self::EACL_APPNAME) as $owner => $rights) - { - $eacls[] = array( - 'path' => $path, - 'owner' => $owner, - 'rights' => $rights, - 'ino' => $stat['ino'], - ); - } - if (($path = egw_vfs::dirname($path))) - { - $eacls = array_merge((array)self::get_eacl($path),$eacls); - } - // sort by length descending, to show precedence - usort($eacls, function($a, $b) { - return strlen($b['path']) - strlen($a['path']); - }); - //error_log(__METHOD__."('$_path') returning ".array2string($eacls)); - return $eacls; - } - - /** - * Return the path of given fs_id(s) - * - * Searches the stat_cache first and then the db. - * Calls itself recursive to to determine the path of the parent/directory - * - * @param int|array $fs_ids integer fs_id or array of them - * @return string|array path or array or pathes indexed by fs_id - */ - static function id2path($fs_ids) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__.'('.array2string($fs_ids).')'); - $ids = (array)$fs_ids; - $pathes = array(); - // first check our stat-cache for the ids - foreach(self::$stat_cache as $path => $stat) - { - if (($key = array_search($stat['fs_id'],$ids)) !== false) - { - $pathes[$stat['fs_id']] = $path; - unset($ids[$key]); - if (!$ids) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__.'('.array2string($fs_ids).')='.array2string($pathes).' *from stat_cache*'); - return is_array($fs_ids) ? $pathes : array_shift($pathes); - } - } - } - // now search via the database - if (count($ids) > 1) array_map(function(&$v) { $v = (int)$v; },$ids); - $query = 'SELECT fs_id,fs_dir,fs_name FROM '.self::TABLE.' WHERE fs_id'. - (count($ids) == 1 ? '='.(int)$ids[0] : ' IN ('.implode(',',$ids).')'); - if (self::LOG_LEVEL > 2) $query = '/* '.__METHOD__.': '.__LINE__.' */ '.$query; - - if (!is_object(self::$pdo)) - { - self::_pdo(); - } - $stmt = self::$pdo->prepare($query); - $stmt->setFetchMode(PDO::FETCH_ASSOC); - if (!$stmt->execute()) - { - return false; // not found - } - $parents = array(); - foreach($stmt as $row) - { - if ($row['fs_dir'] > 1 && !in_array($row['fs_dir'],$parents)) - { - $parents[] = $row['fs_dir']; - } - $rows[$row['fs_id']] = $row; - } - unset($stmt); - - if ($parents && !($parents = self::id2path($parents))) - { - return false; // parent not found, should never happen ... - } - if (self::LOG_LEVEL > 1) error_log(__METHOD__." trying foreach with:".print_r($rows,true)."#"); - foreach((array)$rows as $fs_id => $row) - { - $parent = $row['fs_dir'] > 1 ? $parents[$row['fs_dir']] : ''; - - $pathes[$fs_id] = $parent . '/' . $row['fs_name']; - } - if (self::LOG_LEVEL > 1) error_log(__METHOD__.'('.array2string($fs_ids).')='.array2string($pathes)); - return is_array($fs_ids) ? $pathes : array_shift($pathes); - } - - /** - * Convert a sqlfs-file-info into a stat array - * - * @param array $info - * @return array - */ - static protected function _vfsinfo2stat($info) - { - $stat = array( - 'ino' => $info['fs_id'], - 'name' => $info['fs_name'], - 'mode' => $info['fs_mode'] | - ($info['fs_mime'] == self::DIR_MIME_TYPE ? self::MODE_DIR : - ($info['fs_mime'] == self::SYMLINK_MIME_TYPE ? self::MODE_LINK : self::MODE_FILE)), // required by the stream wrapper - 'size' => $info['fs_size'], - 'uid' => $info['fs_uid'], - 'gid' => $info['fs_gid'], - 'mtime' => strtotime($info['fs_modified']), - 'ctime' => strtotime($info['fs_created']), - 'nlink' => $info['fs_mime'] == self::DIR_MIME_TYPE ? 2 : 1, - // eGW addition to return some extra values - 'mime' => $info['fs_mime'], - 'readlink' => $info['fs_link'], - ); - if (self::LOG_LEVEL > 1) error_log(__METHOD__."($info[name]) = ".array2string($stat)); - return $stat; - } - - public static $pdo_type; - /** - * Case sensitive comparison operator, for mysql we use ' COLLATE utf8_bin =' - * - * @var string - */ - public static $case_sensitive_equal = '='; - - /** - * Reconnect to database - */ - static public function reconnect() - { - self::$pdo = self::_pdo(); - } - - /** - * Create pdo object / connection, as long as pdo is not generally used in eGW - * - * @return PDO - */ - static protected function _pdo() - { - $egw_db = isset($GLOBALS['egw_setup']) ? $GLOBALS['egw_setup']->db : $GLOBALS['egw']->db; - - switch($egw_db->Type) - { - case 'mysqli': - case 'mysqlt': - case 'mysql': - self::$case_sensitive_equal = '= BINARY '; - self::$pdo_type = 'mysql'; - break; - default: - self::$pdo_type = $egw_db->Type; - break; - } - $dsn = self::$pdo_type.':dbname='.$egw_db->Database.($egw_db->Host ? ';host='.$egw_db->Host.($egw_db->Port ? ';port='.$egw_db->Port : '') : ''); - // check once if pdo extension and DB specific driver is loaded or can be loaded - static $pdo_available=null; - if (is_null($pdo_available)) - { - foreach(array('pdo','pdo_'.self::$pdo_type) as $ext) - { - check_load_extension($ext,true); // true = throw Exception - } - $pdo_available = true; - } - try { - self::$pdo = new PDO($dsn,$egw_db->User,$egw_db->Password,array( - PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION, - )); - } - catch(Exception $e) - { - unset($e); - // Exception reveals password, so we ignore the exception and connect again without pw, to get the right exception without pw - self::$pdo = new PDO($dsn,$egw_db->User,'$egw_db->Password'); - } - // set client charset of the connection - $charset = translation::charset(); - switch(self::$pdo_type) - { - case 'mysql': - if (isset($egw_db->Link_ID->charset2mysql[$charset])) $charset = $egw_db->Link_ID->charset2mysql[$charset]; - // fall throught - case 'pgsql': - $query = "SET NAMES '$charset'"; - break; - } - if ($query) - { - self::$pdo->exec($query); - } - return self::$pdo; - } - - /** - * Just a little abstration 'til I know how to organise stuff like that with PDO - * - * @param mixed $time - * @return string Y-m-d H:i:s - */ - static protected function _pdo_timestamp($time) - { - if (is_numeric($time)) - { - $time = date('Y-m-d H:i:s',$time); - } - return $time; - } - - /** - * Just a little abstration 'til I know how to organise stuff like that with PDO - * - * @param boolean $val - * @return string '1' or '0' for mysql, 'true' or 'false' for everyone else - */ - static protected function _pdo_boolean($val) - { - if (self::$pdo_type == 'mysql') - { - return $val ? '1' : '0'; - } - return $val ? 'true' : 'false'; - } - - /** - * Maximum value for a single hash element (should be 10^N): 10, 100 (default), 1000, ... - * - * DONT change this value, once you have files stored, they will no longer be found! - */ - const HASH_MAX = 100; - - /** - * Return the path of the stored content of a file if $this->operation == self::STORE2FS - * - * To limit the number of files stored in one directory, we create a hash from the fs_id: - * 1 --> /00/1 - * 34 --> /00/34 - * 123 --> /01/123 - * 4567 --> /45/4567 - * 99999 --> /09/99/99999 - * --> so one directory contains maximum 2 * HASH_MAY entries (HASH_MAX dirs + HASH_MAX files) - * @param int $id id of the file - * @return string - */ - static function _fs_path($id) - { - if (!is_numeric($id)) - { - throw new egw_exception_wrong_parameter(__METHOD__."(id=$id) id has to be an integer!"); - } - if (!isset($GLOBALS['egw_info']['server']['files_dir'])) - { - if (is_object($GLOBALS['egw_setup']->db)) // if we run under setup, query the db for the files dir - { - $GLOBALS['egw_info']['server']['files_dir'] = $GLOBALS['egw_setup']->db->select('egw_config','config_value',array( - 'config_name' => 'files_dir', - 'config_app' => 'phpgwapi', - ),__LINE__,__FILE__)->fetchColumn(); - } - } - if (!$GLOBALS['egw_info']['server']['files_dir']) - { - throw new egw_exception_assertion_failed("\$GLOBALS['egw_info']['server']['files_dir'] not set!"); - } - $hash = array(); - $n = $id; - while(($n = (int) ($n / self::HASH_MAX))) - { - $hash[] = sprintf('%02d',$n % self::HASH_MAX); - } - if (!$hash) $hash[] = '00'; // we need at least one directory, to not conflict with the dir-names - array_unshift($hash,$id); - - $path = '/sqlfs/'.implode('/',array_reverse($hash)); - //error_log(__METHOD__."($id) = '$path'"); - return $GLOBALS['egw_info']['server']['files_dir'].$path; - } - - /** - * Replace the password of an url with '...' for error messages - * - * @param string &$url - */ - static protected function _remove_password(&$url) - { - $parts = egw_vfs::parse_url($url); - - if ($parts['pass'] || $parts['scheme']) - { - $url = $parts['scheme'].'://'.($parts['user'] ? $parts['user'].($parts['pass']?':...':'').'@' : ''). - $parts['host'].$parts['path']; - } - } - - /** - * Get storage mode from url (get parameter 'storage', eg. ?storage=db) - * - * @param string|array $url complete url or array of url-parts from parse_url - * @return int self::STORE2FS or self::STORE2DB - */ - static function url2operation($url) - { - $operation = self::DEFAULT_OPERATION; - - if (strpos(is_array($url) ? $url['query'] : $url,'storage=') !== false) - { - $query = null; - parse_str(is_array($url) ? $url['query'] : egw_vfs::parse_url($url,PHP_URL_QUERY), $query); - switch ($query['storage']) - { - case 'db': - $operation = self::STORE2DB; - break; - case 'fs': - default: - $operation = self::STORE2FS; - break; - } - } - //error_log(__METHOD__."('$url') = $operation (1=DB, 2=FS)"); - return $operation; - } - - /** - * Store properties for a single ressource (file or dir) - * - * @param string|int $path string with path or integer fs_id - * @param array $props array of array with values for keys 'name', 'ns', 'val' (null to delete the prop) - * @return boolean true if props are updated, false otherwise (eg. ressource not found) - */ - static function proppatch($path,array $props) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__."(".array2string($path).','.array2string($props)); - if (!is_numeric($path)) - { - if (!($stat = self::url_stat($path,0))) - { - return false; - } - $id = $stat['ino']; - } - elseif(!($path = self::id2path($id=$path))) - { - return false; - } - if (!egw_vfs::check_access($path,EGW_ACL_EDIT,$stat)) - { - return false; // permission denied - } - $ins_stmt = $del_stmt = null; - foreach($props as &$prop) - { - if (!isset($prop['ns'])) $prop['ns'] = egw_vfs::DEFAULT_PROP_NAMESPACE; - - if (!isset($prop['val']) || self::$pdo_type != 'mysql') // for non mysql, we have to delete the prop anyway, as there's no REPLACE! - { - if (!isset($del_stmt)) - { - $del_stmt = self::$pdo->prepare('DELETE FROM '.self::PROPS_TABLE.' WHERE fs_id=:fs_id AND prop_namespace=:prop_namespace AND prop_name=:prop_name'); - } - $del_stmt->execute(array( - 'fs_id' => $id, - 'prop_namespace' => $prop['ns'], - 'prop_name' => $prop['name'], - )); - } - if (isset($prop['val'])) - { - if (!isset($ins_stmt)) - { - $ins_stmt = self::$pdo->prepare((self::$pdo_type == 'mysql' ? 'REPLACE' : 'INSERT'). - ' INTO '.self::PROPS_TABLE.' (fs_id,prop_namespace,prop_name,prop_value) VALUES (:fs_id,:prop_namespace,:prop_name,:prop_value)'); - } - if (!$ins_stmt->execute(array( - 'fs_id' => $id, - 'prop_namespace' => $prop['ns'], - 'prop_name' => $prop['name'], - 'prop_value' => $prop['val'], - ))) - { - return false; - } - } - } - return true; - } - - /** - * Read properties for a ressource (file, dir or all files of a dir) - * - * @param array|string|int $path_ids (array of) string with path or integer fs_id - * @param string $ns ='http://egroupware.org/' namespace if propfind should be limited to a single one, use null for all - * @return array|boolean false on error ($path_ids does not exist), array with props (values for keys 'name', 'ns', 'value'), or - * fs_id/path => array of props for $depth==1 or is_array($path_ids) - */ - static function propfind($path_ids,$ns=egw_vfs::DEFAULT_PROP_NAMESPACE) - { - $ids = is_array($path_ids) ? $path_ids : array($path_ids); - foreach($ids as &$id) - { - if (!is_numeric($id)) - { - if (!($stat = self::url_stat($id,0))) - { - if (self::LOG_LEVEL) error_log(__METHOD__."(".array2string($path_ids).",$ns) path '$id' not found!"); - return false; - } - $id = $stat['ino']; - } - } - if (count($ids) >= 1) array_map(function(&$v) { $v = (int)$v; },$ids); - $query = 'SELECT * FROM '.self::PROPS_TABLE.' WHERE (fs_id'. - (count($ids) == 1 ? '='.(int)implode('',$ids) : ' IN ('.implode(',',$ids).')').')'. - (!is_null($ns) ? ' AND prop_namespace=?' : ''); - if (self::LOG_LEVEL > 2) $query = '/* '.__METHOD__.': '.__LINE__.' */ '.$query; - - $stmt = self::$pdo->prepare($query); - $stmt->setFetchMode(PDO::FETCH_ASSOC); - $stmt->execute(!is_null($ns) ? array($ns) : array()); - - $props = array(); - foreach($stmt as $row) - { - $props[$row['fs_id']][] = array( - 'val' => $row['prop_value'], - 'name' => $row['prop_name'], - 'ns' => $row['prop_namespace'], - ); - } - if (!is_array($path_ids)) - { - $props = $props[$row['fs_id']] ? $props[$row['fs_id']] : array(); // return empty array for no props - } - elseif ($props && isset($stat)) // need to map fs_id's to pathes - { - foreach(self::id2path(array_keys($props)) as $id => $path) - { - $props[$path] =& $props[$id]; - unset($props[$id]); - } - } - if (self::LOG_LEVEL > 1) - { - foreach((array)$props as $k => $v) - { - error_log(__METHOD__."($path_ids,$ns) $k => ".array2string($v)); - } - } - return $props; - } -} - -stream_register_wrapper(sqlfs_stream_wrapper::SCHEME ,'sqlfs_stream_wrapper'); +class sqlfs_stream_wrapper extends Sqlfs\StreamWrapper {} diff --git a/phpgwapi/inc/class.sqlfs_utils.inc.php b/phpgwapi/inc/class.sqlfs_utils.inc.php index 67a93cbb48..5533aac921 100644 --- a/phpgwapi/inc/class.sqlfs_utils.inc.php +++ b/phpgwapi/inc/class.sqlfs_utils.inc.php @@ -7,479 +7,13 @@ * @package api * @subpackage vfs * @author Ralf Becker - * @copyright (c) 2008-14 by Ralf Becker + * @copyright (c) 2008-15 by Ralf Becker * @version $Id$ */ -require_once 'class.iface_stream_wrapper.inc.php'; -require_once 'class.sqlfs_stream_wrapper.inc.php'; +use EGroupware\Api\Vfs\Sqlfs; /** - * sqlfs stream wrapper utilities: migration db-fs, fsck + * @depredated use EGroupware\Api\Vfs\Sqlfs\Utils */ -class sqlfs_utils extends sqlfs_stream_wrapper -{ - /** - * Migrate SQLFS content from DB to filesystem - * - * @param boolean $debug true to echo a message for each copied file - */ - static function migrate_db2fs($debug=true) - { - if (!is_object(self::$pdo)) - { - self::_pdo(); - } - $query = 'SELECT fs_id,fs_name,fs_size,fs_content'. - ' FROM '.self::TABLE.' WHERE fs_content IS NOT NULL'. - ' ORDER BY fs_id LIMIT 5 OFFSET :offset'; - - $fs_id = $fs_name = $fs_size = $fs_content = null; - $n = 0; - $stmt = self::$pdo->prepare($query); - $stmt->bindColumn(1,$fs_id); - $stmt->bindColumn(2,$fs_name); - $stmt->bindColumn(3,$fs_size); - $stmt->bindColumn(4,$fs_content,PDO::PARAM_LOB); - $stmt->bindValue(':offset', $n, PDO::PARAM_INT); - - while ($stmt->execute()) - { - foreach($stmt as $row) - { - // hack to work around a current php bug (http://bugs.php.net/bug.php?id=40913) - // PDOStatement::bindColumn(,,PDO::PARAM_LOB) is not working for MySQL, content is returned as string :-( - if (is_string($fs_content)) - { - $name = md5($fs_name.$fs_id); - $GLOBALS[$name] =& $fs_content; - require_once(EGW_API_INC.'/class.global_stream_wrapper.inc.php'); - $content = fopen('global://'.$name,'r'); - if (!$content) echo "fopen('global://$name','w' failed, strlen(\$GLOBALS['$name'])=".strlen($GLOBALS[$name]).", \$GLOBALS['$name']=".substr($GLOBALS['$name'],0,100)."...\n"; - unset($GLOBALS[$name]); // unset it, so it does not use up memory, once the stream is closed - } - else - { - $content = $fs_content; - } - if (!is_resource($content)) - { - throw new egw_exception_assertion_failed(__METHOD__."(): fs_id=$fs_id ($fs_name, $fs_size bytes) content is NO resource! ".array2string($content)); - } - $filename = self::_fs_path($fs_id); - if (!file_exists($fs_dir=egw_vfs::dirname($filename))) - { - self::mkdir_recursive($fs_dir,0700,true); - } - if (!($dest = fopen($filename,'w'))) - { - throw new egw_exception_assertion_failed(__METHOD__."(): fopen($filename,'w') failed!"); - } - if (($bytes = stream_copy_to_stream($content,$dest)) != $fs_size) - { - throw new egw_exception_assertion_failed(__METHOD__."(): fs_id=$fs_id ($fs_name) $bytes bytes copied != size of $fs_size bytes!"); - } - if ($debug) error_log("$fs_id: $fs_name: $bytes bytes copied to fs"); - fclose($dest); - fclose($content); unset($content); - - ++$n; - } - if (!$n) break; // just in case nothing is found, statement will execute just fine - - $stmt->bindValue(':offset', $n, PDO::PARAM_INT); - } - unset($row); // not used, as we access bound variables - unset($stmt); - - if ($n) // delete all content in DB, if there was some AND no error (exception thrown!) - { - $query = 'UPDATE '.self::TABLE.' SET fs_content=NULL'; - $stmt = self::$pdo->prepare($query); - $stmt->execute(); - } - return $n; - } - - /** - * Check and optionaly fix corruption in sqlfs - * - * @param boolean $check_only=true - * @return array with messages / found problems - */ - public static function fsck($check_only=true) - { - if (!is_object(self::$pdo)) - { - self::_pdo(); - } - $msgs = array(); - foreach(array( - self::fsck_fix_required_nodes($check_only), - self::fsck_fix_multiple_active($check_only), - self::fsck_fix_unconnected($check_only), - self::fsck_fix_no_content($check_only), - ) as $check_msgs) - { - if ($check_msgs) $msgs = array_merge($msgs, $check_msgs); - } - - foreach ($GLOBALS['egw']->hooks->process(array( - 'location' => 'fsck', - 'check_only' => $check_only) - ) as $app_msgs) - { - if ($app_msgs) $msgs = array_merge($msgs, $app_msgs); - } - return $msgs; - } - - /** - * Check and optionally create required nodes: /, /home, /apps - * - * @param boolean $check_only=true - * @return array with messages / found problems - */ - private static function fsck_fix_required_nodes($check_only=true) - { - static $dirs = array( - '/' => 1, - '/home' => 2, - '/apps' => 3, - ); - $stmt = $delete_stmt = null; - $msgs = array(); - foreach($dirs as $path => $id) - { - if (!($stat = self::url_stat($path, STREAM_URL_STAT_LINK))) - { - if ($check_only) - { - $msgs[] = lang('Required directory "%1" not found!', $path); - } - else - { - if (!isset($stmt)) - { - $stmt = self::$pdo->prepare('INSERT INTO '.self::TABLE.' (fs_id,fs_name,fs_dir,fs_mode,fs_uid,fs_gid,fs_size,fs_mime,fs_created,fs_modified,fs_creator'. - ') VALUES (:fs_id,:fs_name,:fs_dir,:fs_mode,:fs_uid,:fs_gid,:fs_size,:fs_mime,:fs_created,:fs_modified,:fs_creator)'); - } - try { - $ok = $stmt->execute($data = array( - 'fs_id' => $id, - 'fs_name' => substr($path,1), - 'fs_dir' => $path == '/' ? 0 : $dirs['/'], - 'fs_mode' => 05, - 'fs_uid' => 0, - 'fs_gid' => 0, - 'fs_size' => 0, - 'fs_mime' => 'httpd/unix-directory', - 'fs_created' => self::_pdo_timestamp(time()), - 'fs_modified' => self::_pdo_timestamp(time()), - 'fs_creator' => 0, - )); - } - catch (PDOException $e) - { - $ok = false; - unset($e); // ignore exception - } - if (!$ok) // can not insert it, try deleting it first - { - if (!isset($delete_stmt)) - { - $delete_stmt = self::$pdo->prepare('DELETE FROM '.self::TABLE.' WHERE fs_id=:fs_id'); - } - try { - $ok = $delete_stmt->execute(array('fs_id' => $id)) && $stmt->execute($data); - } - catch (PDOException $e) - { - unset($e); // ignore exception - } - } - $msgs[] = $ok ? lang('Required directory "%1" created.', $path) : - lang('Failed to create required directory "%1"!', $path); - } - } - // check if directory is at least world readable and executable (r-x), we allow more but not less - elseif (($stat['mode'] & 05) != 05) - { - if ($check_only) - { - $msgs[] = lang('Required directory "%1" has wrong mode %2 instead of %3!', - $path, egw_vfs::int2mode($stat['mode']), egw_vfs::int2mode(05|0x4000)); - } - else - { - $stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_mode=:fs_mode WHERE fs_id=:fs_id'); - if (($ok = $stmt->execute(array( - 'fs_id' => $id, - 'fs_mode' => 05, - )))) - { - $msgs[] = lang('Mode of required directory "%1" changed to %2.', $path, egw_vfs::int2mode(05|0x4000)); - } - else - { - $msgs[] = lang('Failed to change mode of required directory "%1" to %2!', $path, egw_vfs::int2mode(05|0x4000)); - } - } - } - } - if (!$check_only && $msgs) - { - global $oProc; - if (!isset($oProc)) $oProc = new schema_proc(); - // PostgreSQL seems to require to update the sequenz, after manually inserting id's - $oProc->UpdateSequence('egw_sqlfs', 'fs_id'); - } - return $msgs; - } - - /** - * Check and optionally remove files without content part in physical filesystem - * - * @param boolean $check_only=true - * @return array with messages / found problems - */ - private static function fsck_fix_no_content($check_only=true) - { - $stmt = null; - $msgs = array(); - foreach(self::$pdo->query('SELECT fs_id FROM '.self::TABLE. - " WHERE fs_mime!='httpd/unix-directory' AND fs_content IS NULL AND fs_link IS NULL") as $row) - { - if (!file_exists($phy_path=self::_fs_path($row['fs_id']))) - { - $path = self::id2path($row['fs_id']); - if ($check_only) - { - $msgs[] = lang('File %1 has no content in physical filesystem %2!', - $path.' (#'.$row['fs_id'].')',$phy_path); - } - else - { - if (!isset($stmt)) - { - $stmt = self::$pdo->prepare('DELETE FROM '.self::TABLE.' WHERE fs_id=:fs_id'); - $stmt_props = self::$pdo->prepare('DELETE FROM '.self::PROPS_TABLE.' WHERE fs_id=:fs_id'); - } - if ($stmt->execute(array('fs_id' => $row['fs_id'])) && - $stmt_props->execute(array('fs_id' => $row['fs_id']))) - { - $msgs[] = lang('File %1 has no content in physical filesystem %2 --> file removed!',$path,$phy_path); - } - else - { - $msgs[] = lang('File %1 has no content in physical filesystem %2 --> failed to remove file!', - $path.' (#'.$row['fs_id'].')',$phy_path); - } - } - } - } - if ($check_only && $msgs) - { - $msgs[] = lang('Files without content in physical filesystem will be removed.'); - } - return $msgs; - } - - /** - * Name of lost+found directory for unconnected nodes - */ - const LOST_N_FOUND = '/lost+found'; - const LOST_N_FOUND_MOD = 070; - const LOST_N_FOUND_GRP = 'Admins'; - - /** - * Check and optionally fix unconnected nodes - parent directory does not (longer) exists: - * - * SELECT fs.* - * FROM egw_sqlfs fs - * LEFT JOIN egw_sqlfs dir ON dir.fs_id=fs.fs_dir - * WHERE fs.fs_id > 1 && dir.fs_id IS NULL - * - * @param boolean $check_only=true - * @return array with messages / found problems - */ - private static function fsck_fix_unconnected($check_only=true) - { - $lostnfound = null; - $msgs = array(); - foreach(self::$pdo->query('SELECT fs.* FROM '.self::TABLE.' fs'. - ' LEFT JOIN '.self::TABLE.' dir ON dir.fs_id=fs.fs_dir'. - ' WHERE fs.fs_id > 1 AND dir.fs_id IS NULL') as $row) - { - if ($check_only) - { - $msgs[] = lang('Found unconnected %1 %2!', - mime_magic::mime2label($row['fs_mime']), - egw_vfs::decodePath($row['fs_name']).' (#'.$row['fs_id'].')'); - continue; - } - if (!isset($lostnfound)) - { - // check if we already have /lost+found, create it if not - if (!($lostnfound = self::url_stat(self::LOST_N_FOUND, STREAM_URL_STAT_QUIET))) - { - egw_vfs::$is_root = true; - if (!self::mkdir(self::LOST_N_FOUND, self::LOST_N_FOUND_MOD, 0) || - !(!($admins = $GLOBALS['egw']->accounts->name2id(self::LOST_N_FOUND_GRP)) || - self::chgrp(self::LOST_N_FOUND, $admins) && self::chmod(self::LOST_N_FOUND,self::LOST_N_FOUND_MOD)) || - !($lostnfound = self::url_stat(self::LOST_N_FOUND, STREAM_URL_STAT_QUIET))) - { - $msgs[] = lang("Can't create directory %1 to connect found unconnected nodes to it!",self::LOST_N_FOUND); - } - else - { - $msgs[] = lang('Successful created new directory %1 for unconnected nods.',self::LOST_N_FOUND); - } - egw_vfs::$is_root = false; - if (!$lostnfound) break; - } - $stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_dir=:fs_dir WHERE fs_id=:fs_id'); - } - if ($stmt->execute(array( - 'fs_dir' => $lostnfound['ino'], - 'fs_id' => $row['fs_id'], - ))) - { - $msgs[] = lang('Moved unconnected %1 %2 to %3.', - mime_magic::mime2label($row['fs_mime']), - egw_vfs::decodePath($row['fs_name']).' (#'.$row['fs_id'].')', - self::LOST_N_FOUND); - } - else - { - $msgs[] = lang('Failed to move unconnected %1 %2 to %3!', - mime_magic::mime2label($row['fs_mime']), egw_vfs::decodePath($row['fs_name']), self::LOST_N_FOUND); - } - } - if ($check_only && $msgs) - { - $msgs[] = lang('Unconnected nodes will be moved to %1.',self::LOST_N_FOUND); - } - return $msgs; - } - - /** - * Check and optionally fix multiple active files and directories with identical path - * - * @param boolean $check_only=true - * @return array with messages / found problems - */ - private static function fsck_fix_multiple_active($check_only=true) - { - $stmt = $inactivate_msg_added = null; - $msgs = array(); - foreach(self::$pdo->query('SELECT fs_dir,fs_name,COUNT(*) FROM '.self::TABLE. - ' WHERE fs_active='.self::_pdo_boolean(true). - ' GROUP BY fs_dir,'.(self::$pdo_type == 'mysql' ? 'BINARY ' : '').'fs_name'. // fs_name is casesensitive! - ' HAVING COUNT(*) > 1') as $row) - { - if (!isset($stmt)) - { - $stmt = self::$pdo->prepare('SELECT *,(SELECT COUNT(*) FROM '.self::TABLE.' sub WHERE sub.fs_dir=fs.fs_id) AS children'. - ' FROM '.self::TABLE.' fs'. - ' WHERE fs.fs_dir=:fs_dir AND fs.fs_active='.self::_pdo_boolean(true).' AND fs.fs_name'.self::$case_sensitive_equal.':fs_name'. - " ORDER BY fs.fs_mime='httpd/unix-directory' DESC,children DESC,fs.fs_modified DESC"); - $inactivate_stmt = self::$pdo->prepare('UPDATE '.self::TABLE. - ' SET fs_active='.self::_pdo_boolean(false). - ' WHERE fs_dir=:fs_dir AND fs_active='.self::_pdo_boolean(true). - ' AND fs_name'.self::$case_sensitive_equal.':fs_name AND fs_id!=:fs_id'); - } - //$msgs[] = array2string($row); - $cnt = 0; - $stmt->execute(array( - 'fs_dir' => $row['fs_dir'], - 'fs_name' => $row['fs_name'], - )); - foreach($stmt as $n => $entry) - { - if ($entry['fs_mime'] == 'httpd/unix-directory') - { - if (!$n) - { - $dir = $entry; // directory to keep - $msgs[] = lang('%1 directories %2 found!', $row[2], self::id2path($entry['fs_id'])); - if ($check_only) break; - } - else - { - if ($entry['children']) - { - $msgs[] = lang('Moved %1 children from directory fs_id=%2 to %3', - $children = self::$pdo->exec('UPDATE '.self::TABLE.' SET fs_dir='.(int)$dir['fs_id']. - ' WHERE fs_dir='.(int)$entry['fs_id']), - $entry['fs_id'], $dir['fs_id']); - - $dir['children'] += $children; - } - self::$pdo->query('DELETE FROM '.self::TABLE.' WHERE fs_id='.(int)$entry['fs_id']); - $msgs[] = lang('Removed (now) empty directory fs_id=%1',$entry['fs_id']); - } - } - elseif (isset($dir)) // file and directory with same name exist! - { - if (!$check_only) - { - $inactivate_stmt->execute(array( - 'fs_dir' => $row['fs_dir'], - 'fs_name' => $row['fs_name'], - 'fs_id' => $dir['fs_id'], - )); - $cnt = $inactivate_stmt->rowCount(); - } - else - { - $cnt = ucfirst(lang('none of %1', $row[2]-1)); - } - $msgs[] = lang('%1 active file(s) with same name as directory inactivated!',$cnt); - break; - } - else // newest file --> set for all other fs_active=false - { - if (!$check_only) - { - $inactivate_stmt->execute(array( - 'fs_dir' => $row['fs_dir'], - 'fs_name' => $row['fs_name'], - 'fs_id' => $entry['fs_id'], - )); - $cnt = $inactivate_stmt->rowCount(); - } - else - { - $cnt = lang('none of %1', $row[2]-1); - } - $msgs[] = lang('More then one active file %1 found, inactivating %2 older revisions!', - self::id2path($entry['fs_id']), $cnt); - break; - } - } - unset($dir); - if ($cnt && !isset($inactivate_msg_added)) - { - $msgs[] = lang('To examine or reinstate inactived files, you might need to turn versioning on.'); - $inactivate_msg_added = true; - } - } - return $msgs; - } -} - -// fsck testcode, if this file is called via it's URL (you need to uncomment it!) -/*if (isset($_SERVER['SCRIPT_FILENAME']) && $_SERVER['SCRIPT_FILENAME'] == __FILE__) -{ - $GLOBALS['egw_info'] = array( - 'flags' => array( - 'currentapp' => 'admin', - 'nonavbar' => true, - ), - ); - include_once '../../header.inc.php'; - - $msgs = sqlfs_utils::fsck(!isset($_GET['check_only']) || $_GET['check_only']); - echo '

'.implode("

\n

", (array)$msgs)."

\n"; -}*/ \ No newline at end of file +class sqlfs_utils extends Sqlfs\Utils {} diff --git a/phpgwapi/inc/common_functions.inc.php b/phpgwapi/inc/common_functions.inc.php index 7a04ede619..5c565168ac 100755 --- a/phpgwapi/inc/common_functions.inc.php +++ b/phpgwapi/inc/common_functions.inc.php @@ -1624,16 +1624,44 @@ function json_php_unserialize($str, $allow_not_serialized=false) } /** - * php5 autoload function for eGroupWare understanding the following naming schema: + * New PSR-4 autoloader for EGroupware + * + * class_exists('\\EGroupware\\Api\\Vfs'); // /api/src/Vfs.php + * class_exists('\\EGroupware\\Api\\Vfs\\Dav\\Directory'); // /api/src/Vfs/Dav/Directory.php + * class_exists('\\EGroupware\\Api\\Vfs\\Sqlfs\\StreamWrapper'); // /api/src/Vfs/Sqlfs/StreamWrapper.php + * class_exists('\\EGroupware\\Api\\Vfs\\Sqlfs\\Utils'); // /api/src/Vfs/Sqlfs/Utils.php + * class_exists('\\EGroupware\\Stylite\\Versioning\\StreamWrapper'); // /stylite/src/Versioning/StreamWrapper.php + * class_exists('\\EGroupware\\Calendar\\Ui'); // /calendar/src/Ui.php + * class_exists('\\EGroupware\\Calendar\\Ui\\Lists'); // /calendar/src/Ui/Lists.php + * class_exists('\\EGroupware\\Calendar\\Ui\\Views'); // /calendar/src/Ui/Views.php + */ +spl_autoload_register(function($class) +{ + $parts = explode('\\', $class); + if (array_shift($parts) != 'EGroupware') return; // not our prefix + + $app = lcfirst(array_shift($parts)); + $base = EGW_INCLUDE_ROOT.'/'.$app.'/src/'; + $path = $base.implode('/', $parts).'.php'; + + if (file_exists($path)) + { + require_once $path; + //error_log("PSR4_autoload('$class') --> require_once($path) --> class_exists('$class')=".array2string(class_exists($class,false))); + } +}); + +/** + * Old autoloader for EGroupware understanding the following naming schema: + * * 1. new (prefered) nameing schema: app_class_something loading app/inc/class.class_something.inc.php * 2. API classes: classname loading phpgwapi/inc/class.classname.inc.php * 2a.API classes containing multiple classes per file eg. egw_exception* in class.egw_exception.inc.php * 3. eTemplate classes: classname loading etemplate/inc/class.classname.inc.php - * 4. classes of the current app: classname loading $GLOBALS['egw_info']['flags']['currentapp']/inc/class.classname.inc.php * * @param string $class name of class to load */ -function __autoload($class) +spl_autoload_register(function($class) { // fixing warnings generated by php 5.3.8 is_a($obj) trying to autoload huge strings if (strlen($class) > 64 || strpos($class, '.') !== false) return; @@ -1668,9 +1696,7 @@ function __autoload($class) { call_user_func($GLOBALS['egw_info']['flags']['autoload'],$class); } -} -// register our autoloader with sql, so other PHP code can use spl_autoload_register, without stalling our autoloader -spl_autoload_register('__autoload'); +}); // if we have a Composer vendor directory, also load it's autoloader, to allow manage our requirements with Composer if (file_exists(EGW_SERVER_ROOT.'/vendor'))