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!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 Beckeregw_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'.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'))