From 70b603ac7714854142c03cc5da110abea177c02f Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Mon, 26 Jan 2015 09:15:07 +0000 Subject: [PATCH] moving VFS API classes into a namespaced PSR4 autoloadable structure: - PSR4 autoloader exists beside our old autloader to support old as well as new structure until everything is ported over - moved ported API stuff from phpgwapi to new api directory (idea is phpgwapi become a compatibility layer for old code, while we only port selected stuff to new api directory) - namespaces use prefix "EGroupware", then (first letter capitalised) app-name or "Api", sub-system names like "Vfs" or for apps "Ui", "Bo, "So" and at least class name starting with a capital letter and without understores eg. "StreamWrapper" plus just ".php" - examples: + egw_vfs in phpgwapi/inc/class.egw_vfs.inc.php --> EGroupware\Api\Vfs in api/src/Vfs.php + sqlfs_stream_wrapper in phpgwapi/inc/class.sqlfs_stream_wrapper.inc.php --> EGroupware\Api\Vfs\Sqlfs\StreamWrapper in api/src/Vfs/Sqlfs/StreamWrapper.php + sqlfs_utils in phpgwapi/inc/class.sqlfs_utils.inc.php --> EGroupware\Api\Vfs\Sqlfs\Utils in api/src/Vfs/Sqlfs/Utils.php - api directory is no a new svn module but exists (like home) as sub-directory under base egroupware module --- api/src/Vfs.php | 2116 +++++++++++++++++ .../src/Vfs/Filesystem/StreamWrapper.php | 122 +- .../src/Vfs/Links/StreamWrapper.php | 87 +- api/src/Vfs/Sqlfs/StreamWrapper.php | 1939 +++++++++++++++ api/src/Vfs/Sqlfs/Utils.php | 483 ++++ .../src/Vfs/StreamWrapper.php | 69 +- api/src/Vfs/StreamWrapperIface.php | 250 ++ etemplate/inc/class.etemplate_new.inc.php | 2 +- phpgwapi/inc/class.egw_vfs.inc.php | 2089 +--------------- .../inc/class.iface_stream_wrapper.inc.php | 241 +- .../inc/class.sqlfs_stream_wrapper.inc.php | 1908 +-------------- phpgwapi/inc/class.sqlfs_utils.inc.php | 469 +--- phpgwapi/inc/common_functions.inc.php | 38 +- 13 files changed, 5008 insertions(+), 4805 deletions(-) create mode 100644 api/src/Vfs.php rename phpgwapi/inc/class.filesystem_stream_wrapper.inc.php => api/src/Vfs/Filesystem/StreamWrapper.php (86%) rename phpgwapi/inc/class.links_stream_wrapper.inc.php => api/src/Vfs/Links/StreamWrapper.php (80%) create mode 100644 api/src/Vfs/Sqlfs/StreamWrapper.php create mode 100644 api/src/Vfs/Sqlfs/Utils.php rename phpgwapi/inc/class.vfs_stream_wrapper.inc.php => api/src/Vfs/StreamWrapper.php (95%) create mode 100644 api/src/Vfs/StreamWrapperIface.php diff --git a/api/src/Vfs.php b/api/src/Vfs.php new file mode 100644 index 0000000000..bfe0ab9523 --- /dev/null +++ b/api/src/Vfs.php @@ -0,0 +1,2116 @@ + + * @copyright (c) 2008-15 by Ralf Becker + * @version $Id$ + */ + +namespace EGroupware\Api; + +// explicitly import old phpgwapi classes used: +use mime_magic; +use common; +use config; +use html; +use egw_db; +use translation; +use HTTP_WebDAV_Server; +use egw_exception_assertion_failed; +use egw_exception_db; +use egw_exception_wrong_parameter; + +/** + * 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: + * + * All examples require a: use EGroupware\Api\Vfs; + * + * - resource Vfs::fopen($path,$mode) like fopen, returned resource can be used with fwrite etc. + * - resource Vfs::opendir($path) like opendir, returned resource can be used with readdir etc. + * - boolean Vfs::copy($from,$to) like copy + * - boolean Vfs::rename($old,$new) renaming or moving a file in the vfs + * - boolean Vfs::mkdir($path) creating a new dir in the vfs + * - boolean Vfs::rmdir($path) removing (an empty) directory + * - boolean Vfs::unlink($path) removing a file + * - boolean Vfs::touch($path,$mtime=null) touch a file + * - boolean Vfs::stat($path) returning status of file like stat(), but only with string keys (no numerical indexes)! + * + * With the exception of 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 Vfs::mount($url,$path) to mount $ur on $path or to return the fstab when called without argument + * - boolean 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 = 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 = Vfs::fopen('/home/user/fromfile','r'); + * $to = Vfs::fopen('/home/user/tofile','w'); + * + * stream_copy_to_stream($from,$to); + * + * The static 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. + * + * Vfs::parse_url($url, $component=-1), Vfs::dirname($url) and Vfs::basename($url) work + * on urls containing utf-8 characters, which get NOT urlencoded in our VFS! + */ +class Vfs extends Vfs\StreamWrapper +{ + 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']; + } + 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] != '/' || !self::stat($path)) continue; + $path = self::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=".self::$find_total.", limit=$options[limit] --> start=$start, limit=$limit
\n"; + + if ((int)$start || self::$find_total > $limit) + { + $result = array_slice($result,(int)$start,(int)$limit,true); + } + } + //echo $path; _debug_array($result); + if ($exec !== true && is_callable($exec)) + { + if (!is_array($exec_params)) + { + $exec_params = is_null($exec_params) ? array() : array($exec_params); + } + foreach($result as $path => &$stat) + { + $options = $exec_params; + array_unshift($options,$path); + array_push($options,$stat); + //echo "calling ".print_r($exec,true).print_r($options,true)."\n"; + $stat = call_user_func_array($exec,$options); + } + return $result; + } + //error_log("self::find($path)=".print_r(array_keys($result),true)); + if ($exec !== true) + { + return array_keys($result); + } + return $result; + } + + /** + * Function carying out the various (optional) checks, before files&dirs get returned as result of find + * + * @param array $options options, see self::find(,$options) + * @param string $path name of path to add + * @param array &$result here we add the stat for the key $path, if the checks are successful + */ + private static function _check_add($options,$path,&$result) + { + $type = $options['type']; // 'd' or 'f' + + if ($options['url']) + { + $stat = @lstat($path); + } + else + { + $stat = self::url_stat($path,STREAM_URL_STAT_LINK); + } + if (!$stat) + { + return; // not found, should not happen + } + if ($type && (($type == 'd') == !($stat['mode'] & Vfs\Sqlfs\StreamWrapper::MODE_DIR) || // != is_dir() which can be true for symlinks + $type == 'F' && is_dir($path))) // symlink to a directory + { + return; // wrong type + } + $stat = array_slice($stat,13); // remove numerical indices 0-12 + $stat['path'] = self::parse_url($path,PHP_URL_PATH); + $stat['name'] = $options['remove'] > 0 ? implode('/',array_slice(explode('/',$stat['path']),$options['remove'])) : self::basename($path); + + if ($options['mime'] || $options['need_mime']) + { + $stat['mime'] = self::mime_content_type($path); + } + if (isset($options['name_preg']) && !preg_match($options['name_preg'],$stat['name']) || + isset($options['path_preg']) && !preg_match($options['path_preg'],$path)) + { + //echo "

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

\n"; + return; // wrong name or path + } + if (isset($options['gid']) && $stat['gid'] != $options['gid'] || + isset($options['uid']) && $stat['uid'] != $options['uid']) + { + return; // wrong user or group + } + if (isset($options['mime']) && $options['mime'] != $stat['mime']) + { + if ($options['mime'][0] == '/') // perl regular expression given + { + if (!preg_match($options['mime'], $stat['mime'])) + { + return; // wrong mime-type + } + } + else + { + list($type,$subtype) = explode('/',$options['mime']); + // no subtype (eg. 'image') --> check only the main type + if ($subtype || substr($stat['mime'],0,strlen($type)+1) != $type.'/') + { + return; // wrong mime-type + } + } + } + if (isset($options['size']) && !self::_check_num($stat['size'],$options['size']) || + (isset($options['empty']) && !!$options['empty'] !== !$stat['size'])) + { + return; // wrong size + } + if (isset($options['cmin']) && !self::_check_num(round((time()-$stat['ctime'])/60),$options['cmin']) || + isset($options['mmin']) && !self::_check_num(round((time()-$stat['mtime'])/60),$options['mmin']) || + isset($options['ctime']) && !self::_check_num(round((time()-$stat['ctime'])/86400),$options['ctime']) || + isset($options['mtime']) && !self::_check_num(round((time()-$stat['mtime'])/86400),$options['mtime'])) + { + return; // not create/modified in the spezified time + } + // do we return url or just vfs pathes + if (!$options['url']) + { + $path = self::parse_url($path,PHP_URL_PATH); + } + $result[$path] = $stat; + } + + private static function _check_num($value,$argument) + { + if (is_int($argument) && $argument >= 0 || $argument[0] != '-' && $argument[0] != '+') + { + //echo "_check_num($value,$argument) check = == ".(int)($value == $argument)."\n"; + return $value == $argument; + } + if ($argument < 0) + { + //echo "_check_num($value,$argument) check < == ".(int)($value < abs($argument))."\n"; + return $value < abs($argument); + } + //echo "_check_num($value,$argument) check > == ".(int)($value > (int)substr($argument,1))."\n"; + return $value > (int) substr($argument,1); + } + + /** + * Recursiv remove all given url's, including it's content if they are files + * + * @param string|array $urls url or array of url's + * @param boolean $allow_urls =false allow to use url's, default no only pathes (to stay within the vfs) + * @throws egw_exception_assertion_failed when trainig to remove /, /apps or /home + * @return array + */ + static function remove($urls,$allow_urls=false) + { + //error_log(__METHOD__.'('.array2string($urls).')'); + // some precaution to never allow to (recursivly) remove /, /apps or /home + foreach((array)$urls as $url) + { + if (preg_match('/^\/?(home|apps|)\/*$/',self::parse_url($url,PHP_URL_PATH))) + { + throw new egw_exception_assertion_failed(__METHOD__.'('.array2string($urls).") Cautiously rejecting to remove folder '$url'!"); + } + } + return self::find($urls,array('depth'=>true,'url'=>$allow_urls,'hidden'=>true),array(__CLASS__,'_rm_rmdir')); + } + + /** + * Helper function for remove: either rmdir or unlink given url (depending if it's a dir or file) + * + * @param string $url + * @return boolean + */ + static function _rm_rmdir($url) + { + if ($url[0] == '/') + { + $url = self::PREFIX . $url; + } + if (is_dir($url) && !is_link($url)) + { + return self::rmdir($url,0); + } + return self::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 = self::READABLE, + * 2 = self::WRITABLE, 1 = self::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 = self::READABLE, + * 2 = self::WRITABLE, 1 = self::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 (self::$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 : self::parse_url($url, PHP_URL_PATH), + 'owner' => $owner ? $owner : self::$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 = self::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 == self::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 = self::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 && + !(self::is_writable($path) || !self::file_exists($path) && self::is_writable(self::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 && !self::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 self::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 self::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 = self::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 = self::stat($file)) && $stat['size'] < 1500000) + { + if (substr($file, 0, 6) == '/apps/') + { + $file = self::parse_url(self::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("/", self::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] == '/' && self::is_dir($path) && self::check_access($path, self::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 = self::concat($dst, self::basename($file)); + + if (self::is_dir($file)) + { + if ($file !== $target) + { + // Create the target directory + self::mkdir($target,null,STREAM_MKDIR_RECURSIVE); + + $copied[] = $file; + $copied[] = $target; // < newly created folder must not be copied again! + if (self::copy_files(self::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 && self::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 (self::is_dir($dst)) + { + foreach($src as $file) + { + $target = self::concat($dst, self::basename($file)); + + if ($file != $target && self::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; + } +} + +Vfs::init_static(); diff --git a/phpgwapi/inc/class.filesystem_stream_wrapper.inc.php b/api/src/Vfs/Filesystem/StreamWrapper.php similarity index 86% rename from phpgwapi/inc/class.filesystem_stream_wrapper.inc.php rename to api/src/Vfs/Filesystem/StreamWrapper.php index 8be319a0e4..edd21f2708 100644 --- a/phpgwapi/inc/class.filesystem_stream_wrapper.inc.php +++ b/api/src/Vfs/Filesystem/StreamWrapper.php @@ -1,16 +1,20 @@ - * @copyright (c) 2008-14 by Ralf Becker + * @copyright (c) 2008-15 by Ralf Becker * @version $Id$ */ +namespace EGroupware\Api\Vfs\Filesystem; + +use EGroupware\Api\Vfs; + /** * eGroupWare API: VFS - stream wrapper to access the regular filesystem (setting a given user, group and mode) * @@ -33,12 +37,12 @@ * * (admin / secret is username / password of setup user, "root_" prefix differenciate from regular EGw-user!) * - * To correctly support characters with special meaning in url's (#?%), we urlencode them with egw_vfs::encodePathComponent + * To correctly support characters with special meaning in url's (#?%), we urlencode them with Vfs::encodePathComponent * and urldecode all path again, before passing them to php's filesystem functions. * * @link http://www.php.net/manual/en/function.stream-wrapper-register.php */ -class filesystem_stream_wrapper implements iface_stream_wrapper +class StreamWrapper implements Vfs\StreamWrapperIface { /** * Scheme / protocol used for this stream-wrapper @@ -47,7 +51,7 @@ class filesystem_stream_wrapper implements iface_stream_wrapper /** * Mime type of directories, the old vfs used 'Directory', while eg. WebDAV uses 'httpd/unix-directory' */ - const DIR_MIME_TYPE = egw_vfs::DIR_MIME_TYPE ; + const DIR_MIME_TYPE = Vfs::DIR_MIME_TYPE ; /** * mode-bits, which have to be set for files @@ -121,16 +125,18 @@ class filesystem_stream_wrapper implements iface_stream_wrapper */ function stream_open ( $url, $mode, $options, &$opened_path ) { + unset($opened_path); // not used, but required by interface + $this->opened_stream = $this->opened_stream_url = null; $read_only = str_replace('b','',$mode) == 'r'; // check access rights, based on the eGW mount perms if (!($stat = self::url_stat($url,0)) || $mode[0] == 'x') // file not found or file should NOT exist { - $dir = egw_vfs::dirname($url); + $dir = Vfs::dirname($url); if ($mode[0] == 'r' || // does $mode require the file to exist (r,r+) $mode[0] == 'x' || // or file should not exist, but does - !egw_vfs::check_access($dir,egw_vfs::WRITABLE,$dir_stat=self::url_stat($dir,0))) // or we are not allowed to create it + !Vfs::check_access($dir,Vfs::WRITABLE,$dir_stat=self::url_stat($dir,0))) // or we are not allowed to create it { if (self::LOG_LEVEL) error_log(__METHOD__."($url,$mode,$options) file does not exist or can not be created!"); if (!($options & STREAM_URL_STAT_QUIET)) @@ -140,7 +146,7 @@ class filesystem_stream_wrapper implements iface_stream_wrapper return false; } } - elseif (!$read_only && !egw_vfs::check_access($url,egw_vfs::WRITABLE,$stat)) // we are not allowed to edit it + elseif (!$read_only && !Vfs::check_access($url,Vfs::WRITABLE,$stat)) // we are not allowed to edit it { if (self::LOG_LEVEL) error_log(__METHOD__."($url,$mode,$options) file can not be edited!"); if (!($options & STREAM_URL_STAT_QUIET)) @@ -160,7 +166,7 @@ class filesystem_stream_wrapper implements iface_stream_wrapper } // open the "real" file - if (!($this->opened_stream = fopen($path=egw_vfs::decodePath(egw_vfs::parse_url($url,PHP_URL_PATH)),$mode,$options))) + if (!($this->opened_stream = fopen($path=Vfs::decodePath(Vfs::parse_url($url,PHP_URL_PATH)),$mode,$options))) { if (self::LOG_LEVEL) error_log(__METHOD__."($url,$mode,$options) fopen('$path','$mode',$options) returned false!"); return false; @@ -302,10 +308,10 @@ class filesystem_stream_wrapper implements iface_stream_wrapper */ static function unlink ( $url ) { - $path = egw_vfs::decodePath(egw_vfs::parse_url($url,PHP_URL_PATH)); + $path = Vfs::decodePath(Vfs::parse_url($url,PHP_URL_PATH)); // check access rights (file need to exist and directory need to be writable - if (!file_exists($path) || is_dir($path) || !egw_vfs::check_access(egw_vfs::dirname($url),egw_vfs::WRITABLE)) + if (!file_exists($path) || is_dir($path) || !Vfs::check_access(Vfs::dirname($url),Vfs::WRITABLE)) { if (self::LOG_LEVEL) error_log(__METHOD__."($url) permission denied!"); return false; // no permission or file does not exist @@ -327,17 +333,17 @@ class filesystem_stream_wrapper implements iface_stream_wrapper */ static function rename ( $url_from, $url_to ) { - $from = egw_vfs::parse_url($url_from); - $to = egw_vfs::parse_url($url_to); + $from = Vfs::parse_url($url_from); + $to = Vfs::parse_url($url_to); // check access rights - if (!($from_stat = self::url_stat($url_from,0)) || !egw_vfs::check_access(egw_vfs::dirname($url_from),egw_vfs::WRITABLE)) + if (!($from_stat = self::url_stat($url_from,0)) || !Vfs::check_access(Vfs::dirname($url_from),Vfs::WRITABLE)) { if (self::LOG_LEVEL) error_log(__METHOD__."($url_from,$url_to): $from[path] permission denied!"); return false; // no permission or file does not exist } - $to_dir = egw_vfs::dirname($url_to); - if (!egw_vfs::check_access($to_dir,egw_vfs::WRITABLE,$to_dir_stat = self::url_stat($to_dir,0))) + $to_dir = Vfs::dirname($url_to); + if (!Vfs::check_access($to_dir,Vfs::WRITABLE,$to_dir_stat = self::url_stat($to_dir,0))) { if (self::LOG_LEVEL) error_log(__METHOD__."($url_from,$url_to): $to_dir permission denied!"); return false; // no permission or parent-dir does not exist @@ -357,12 +363,12 @@ class filesystem_stream_wrapper implements iface_stream_wrapper return false; // no permission or file does not exist } // if destination file already exists, delete it - if ($to_stat && !self::unlink($url_to,$operation)) + if ($to_stat && !self::unlink($url_to)) { if (self::LOG_LEVEL) error_log(__METHOD__."($url_to,$url_from) can't unlink existing $url_to!"); return false; } - return rename(egw_vfs::decodePath($from['path']),egw_vfs::decodePath($to['path'])); + return rename(Vfs::decodePath($from['path']),Vfs::decodePath($to['path'])); } /** @@ -378,24 +384,26 @@ class filesystem_stream_wrapper implements iface_stream_wrapper */ static function mkdir ( $url, $mode, $options ) { - $path = egw_vfs::decodePath(egw_vfs::parse_url($url,PHP_URL_PATH)); + unset($mode); // not used, but required by interface + + $path = Vfs::decodePath(Vfs::parse_url($url,PHP_URL_PATH)); $recursive = (bool)($options & STREAM_MKDIR_RECURSIVE); // find the real parent (might be more then one level if $recursive!) do { $parent = dirname($parent ? $parent : $path); - $parent_url = egw_vfs::dirname($parent_url ? $parent_url : $url); + $parent_url = Vfs::dirname($parent_url ? $parent_url : $url); } while ($recursive && $parent != '/' && !file_exists($parent)); - //echo __METHOD__."($url,$mode,$options) path=$path, recursive=$recursive, parent=$parent, egw_vfs::check_access(parent_url=$parent_url,egw_vfs::WRITABLE)=".(int)egw_vfs::check_access($parent_url,egw_vfs::WRITABLE)."\n"; + //echo __METHOD__."($url,$mode,$options) path=$path, recursive=$recursive, parent=$parent, Vfs::check_access(parent_url=$parent_url,Vfs::WRITABLE)=".(int)Vfs::check_access($parent_url,Vfs::WRITABLE)."\n"; // check access rights (in real filesystem AND by mount perms) - if (file_exists($path) || !file_exists($parent) || !is_writable($parent) || !egw_vfs::check_access($parent_url,egw_vfs::WRITABLE)) + if (file_exists($path) || !file_exists($parent) || !is_writable($parent) || !Vfs::check_access($parent_url,Vfs::WRITABLE)) { if (self::LOG_LEVEL) error_log(__METHOD__."($url) permission denied!"); return false; } - return mkdir($path,$mode=0700,$recursive); // setting mode 0700 allows (only) apache to write into the dir + return mkdir($path, 0700, $recursive); // setting mode 0700 allows (only) apache to write into the dir } /** @@ -410,11 +418,13 @@ class filesystem_stream_wrapper implements iface_stream_wrapper */ static function rmdir ( $url, $options ) { - $path = egw_vfs::decodePath(egw_vfs::parse_url($url,PHP_URL_PATH)); + unset($options); // not used, but required by interface + + $path = Vfs::decodePath(Vfs::parse_url($url,PHP_URL_PATH)); $parent = dirname($path); // check access rights (in real filesystem AND by mount perms) - if (!file_exists($path) || !is_writable($parent) || !egw_vfs::check_access(egw_vfs::dirname($url),egw_vfs::WRITABLE)) + if (!file_exists($path) || !is_writable($parent) || !Vfs::check_access(Vfs::dirname($url),Vfs::WRITABLE)) { if (self::LOG_LEVEL) error_log(__METHOD__."($url) permission denied!"); return false; @@ -425,18 +435,18 @@ class filesystem_stream_wrapper implements iface_stream_wrapper /** * This is not (yet) a stream-wrapper function, but it's necessary and can be used static * - * @param string $path - * @param int $time=null modification time (unix timestamp), default null = current time - * @param int $atime=null access time (unix timestamp), default null = current time, not implemented in the vfs! + * @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! * @return boolean true on success, false otherwise */ static function touch($url,$time=null,$atime=null) { - $path = egw_vfs::decodePath(egw_vfs::parse_url($url,PHP_URL_PATH)); + $path = Vfs::decodePath(Vfs::parse_url($url,PHP_URL_PATH)); $parent = dirname($path); // check access rights (in real filesystem AND by mount perms) - if (!file_exists($path) || !is_writable($parent) || !egw_vfs::check_access(egw_vfs::dirname($url),egw_vfs::WRITABLE)) + if (!file_exists($path) || !is_writable($parent) || !Vfs::check_access(Vfs::dirname($url),Vfs::WRITABLE)) { if (self::LOG_LEVEL) error_log(__METHOD__."($url) permission denied!"); return false; @@ -450,11 +460,13 @@ class filesystem_stream_wrapper implements iface_stream_wrapper * Not supported, as it would require root rights! * * @param string $path - * @param string $mode mode string see egw_vfs::mode2int + * @param string $mode mode string see Vfs::mode2int * @return boolean true on success, false otherwise */ static function chmod($path,$mode) { + unset($path, $mode); // not used, but required by interface + return false; } @@ -469,6 +481,8 @@ class filesystem_stream_wrapper implements iface_stream_wrapper */ static function chown($path,$owner) { + unset($path, $owner); // not used, but required by interface + return false; } @@ -483,6 +497,8 @@ class filesystem_stream_wrapper implements iface_stream_wrapper */ static function chgrp($path,$group) { + unset($path, $group); // not used, but required by interface + return false; } @@ -499,7 +515,7 @@ class filesystem_stream_wrapper implements iface_stream_wrapper $this->opened_dir = null; - $path = egw_vfs::decodePath(egw_vfs::parse_url($this->opened_dir_url = $url,PHP_URL_PATH)); + $path = Vfs::decodePath(Vfs::parse_url($this->opened_dir_url = $url,PHP_URL_PATH)); // ToDo: check access rights @@ -539,18 +555,18 @@ class filesystem_stream_wrapper implements iface_stream_wrapper */ static function url_stat ( $url, $flags ) { - $parts = egw_vfs::parse_url($url); - $path = egw_vfs::decodePath($parts['path']); + $parts = Vfs::parse_url($url); + $path = Vfs::decodePath($parts['path']); $stat = @stat($path); // suppressed the stat failed warnings if ($stat) { // set owner, group and mode from mount options + $uid = $gid = $mode = null; if (!self::parse_query($parts['query'],$uid,$gid,$mode)) { return false; - if (self::LOG_LEVEL > 0) error_log(__METHOD__."($url,$flags) can NOT self::parse_query('$parts[query]')!"); } $stat['uid'] = $stat[4] = $uid; $stat['gid'] = $stat[5] = $gid; @@ -561,7 +577,7 @@ class filesystem_stream_wrapper implements iface_stream_wrapper $stat['mode'] = $stat[2] = $stat['mode'] & ~0222; } } - if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url,$flags) path=$path, mount_mode=".sprintf('0%o',$mode).", mode=".sprintf('0%o',$stat['mode']).'='.egw_vfs::int2mode($stat['mode'])); + if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url,$flags) path=$path, mount_mode=".sprintf('0%o',$mode).", mode=".sprintf('0%o',$stat['mode']).'='.Vfs::int2mode($stat['mode'])); return $stat; } @@ -587,7 +603,7 @@ class filesystem_stream_wrapper implements iface_stream_wrapper while ($ignore); // encode special chars messing up url's - if ($file !== false) $file = egw_vfs::encodePathComponent($file); + if ($file !== false) $file = Vfs::encodePathComponent($file); if (self::LOG_LEVEL > 1) error_log(__METHOD__.'() returning '.array2string($file)); @@ -634,6 +650,7 @@ class filesystem_stream_wrapper implements iface_stream_wrapper */ static function parse_query($query,&$uid,&$gid,&$mode) { + $params = null; parse_str(is_array($query) ? $query['query'] : $query,$params); // setting the default perms root.root r-x for other @@ -690,7 +707,7 @@ class filesystem_stream_wrapper implements iface_stream_wrapper $gid = (int)$value; break; case 'mode': - $mode = egw_vfs::mode2int($value); + $mode = Vfs::mode2int($value); break; case 'url': // ignored, only used for download_url method @@ -712,7 +729,8 @@ class filesystem_stream_wrapper implements iface_stream_wrapper */ static function deny_script($url) { - $parts = egw_vfs::parse_url($url); + $parts = Vfs::parse_url($url); + $get = null; parse_str($parts['query'],$get); $deny = !$get['exec'] && preg_match(self::SCRIPT_EXTENSIONS_PREG,$parts['path']); @@ -730,24 +748,26 @@ class filesystem_stream_wrapper implements iface_stream_wrapper * We use our webdav handler as download url instead of an own download method. * The webdav hander (filemanager/webdav.php) recognices eGW's session cookie and of cause understands regular GET requests. * - * @param string $url - * @param boolean $force_download=false add header('Content-disposition: filename="' . basename($path) . '"'), currently not supported! + * @param string $_url + * @param boolean $force_download =false add header('Content-disposition: filename="' . basename($path) . '"'), currently not supported! * @todo get $force_download working through webdav * @return string|false string with full download url or false to use default webdav.php url */ - static function download_url($url,$force_download=false) + static function download_url($_url,$force_download=false) { - list(,$query) = explode('?',$url,2); + unset($force_download); // not used, but required by interface + + list($url,$query) = explode('?',$_url,2); + $get = null; parse_str($query,$get); if (empty($get['url'])) return false; // no download url given for this mount-point - if (!($mount_url = egw_vfs::mount_url($url))) return false; // no mount url found, should not happen + if (!($mount_url = Vfs::mount_url($_url))) return false; // no mount url found, should not happen list($mount_url) = explode('?',$mount_url); - list($url,$query) = explode('?',$url,2); $relpath = substr($url,strlen($mount_url)); - $download_url = egw_vfs::concat($get['url'],$relpath); + $download_url = Vfs::concat($get['url'],$relpath); if ($download_url[0] == '/') { $download_url = ($_SERVER['HTTPS'] ? 'https://' : 'http://'). @@ -757,6 +777,14 @@ class filesystem_stream_wrapper implements iface_stream_wrapper //die(__METHOD__."('$url') --> relpath = $relpath --> $download_url"); return $download_url; } + + /** + * Register our stream-wrapper + */ + public static function register() + { + stream_register_wrapper(self::SCHEME, __CLASS__); + } } -stream_register_wrapper(filesystem_stream_wrapper::SCHEME ,'filesystem_stream_wrapper'); +StreamWrapper::register(); diff --git a/phpgwapi/inc/class.links_stream_wrapper.inc.php b/api/src/Vfs/Links/StreamWrapper.php similarity index 80% rename from phpgwapi/inc/class.links_stream_wrapper.inc.php rename to api/src/Vfs/Links/StreamWrapper.php index 5ebeb33dc7..bf72f10331 100644 --- a/phpgwapi/inc/class.links_stream_wrapper.inc.php +++ b/api/src/Vfs/Links/StreamWrapper.php @@ -7,18 +7,26 @@ * @package api * @subpackage vfs * @author Ralf Becker - * @copyright (c) 2008-14 by Ralf Becker + * @copyright (c) 2008-15 by Ralf Becker * @version $Id: class.sqlfs_stream_wrapper.inc.php 24997 2008-03-02 21:44:15Z ralfbecker $ */ +namespace EGroupware\Api\Vfs\Links; + +use EGroupware\Api\Vfs; + +// explicitly import old phpgwapi classes used: +use egw_link; +use addressbook_vcal; + /** - * Define parent for links_stream_wrapper, if not already defined + * Define parent for Vfs\Links\StreamWrapper, if not already defined * - * Allows to base links_stream_wrapper on an other wrapper + * Allows to base Vfs\Links\StreamWrapper on an other wrapper */ -if (!class_exists('links_stream_wrapper_parent',false)) +if (!class_exists('EGroupware\\Api\\Vfs\\Links\\LinksParent', false)) { - class links_stream_wrapper_parent extends sqlfs_stream_wrapper {} + class LinksParent extends Vfs\Sqlfs\StreamWrapper {} } /** @@ -42,7 +50,7 @@ if (!class_exists('links_stream_wrapper_parent',false)) * * @link http://www.php.net/manual/en/function.stream-wrapper-register.php */ -class links_stream_wrapper extends links_stream_wrapper_parent +class StreamWrapper extends LinksParent { /** * Scheme / protocoll used for this stream-wrapper @@ -70,11 +78,11 @@ class links_stream_wrapper extends links_stream_wrapper_parent */ static function check_extended_acl($url,$check) { - if (egw_vfs::$is_root) + if (Vfs::$is_root) { return true; } - $path = egw_vfs::parse_url($url,PHP_URL_PATH); + $path = Vfs::parse_url($url,PHP_URL_PATH); list(,$apps,$app,$id,$rel_path) = explode('/',$path,5); @@ -85,7 +93,7 @@ class links_stream_wrapper extends links_stream_wrapper_parent } elseif (!$app) { - $access = !($check & egw_vfs::WRITABLE); // always grant read access to /apps + $access = !($check & Vfs::WRITABLE); // always grant read access to /apps $what = '!$app'; } elseif(!isset($GLOBALS['egw_info']['user']['apps'][$app])) @@ -104,11 +112,11 @@ class links_stream_wrapper extends links_stream_wrapper_parent else { // vfs & stream-wrapper use posix rights, egw_link::file_access uses EGW_ACL_{EDIT|READ}! - $required = $check & egw_vfs::WRITABLE ? EGW_ACL_EDIT : EGW_ACL_READ; - $access = egw_link::file_access($app,$id,$required,$rel_path,egw_vfs::$user); - $what = "from egw_link::file_access('$app',$id,$required,'$rel_path,".egw_vfs::$user.")"; + $required = $check & Vfs::WRITABLE ? EGW_ACL_EDIT : EGW_ACL_READ; + $access = egw_link::file_access($app,$id,$required,$rel_path,Vfs::$user); + $what = "from egw_link::file_access('$app',$id,$required,'$rel_path,".Vfs::$user.")"; } - if (self::DEBUG) error_log(__METHOD__."($url,$check) user=".egw_vfs::$user." ($what) ".($access?"access granted ($app:$id:$rel_path)":'no access!!!')); + if (self::DEBUG) error_log(__METHOD__."($url,$check) user=".Vfs::$user." ($what) ".($access?"access granted ($app:$id:$rel_path)":'no access!!!')); return $access; } @@ -118,7 +126,7 @@ class links_stream_wrapper extends links_stream_wrapper_parent * Reimplemented from sqlfs, as we have to pass the value of check_extends_acl(), due to the lack of late static binding. * And to return vcard for url /apps/addressbook/$id/.entry * - * @param string $path + * @param 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, @@ -131,7 +139,7 @@ class links_stream_wrapper extends links_stream_wrapper_parent */ static function url_stat ( $url, $flags ) { - $eacl_check=self::check_extended_acl($url,egw_vfs::READABLE); + $eacl_check=self::check_extended_acl($url,Vfs::READABLE); // return vCard as /.entry if ( $eacl_check && substr($url,-7) == '/.entry' && @@ -140,7 +148,7 @@ class links_stream_wrapper extends links_stream_wrapper_parent $ret = array( 'ino' => md5($url), 'name' => '.entry', - 'mode' => self::MODE_FILE|egw_vfs::READABLE, // required by the stream wrapper + 'mode' => self::MODE_FILE|Vfs::READABLE, // required by the stream wrapper 'size' => 1024, // fmail does NOT attach files with size 0! 'uid' => 0, 'gid' => 0, @@ -154,7 +162,7 @@ class links_stream_wrapper extends links_stream_wrapper_parent // if entry directory does not exist --> return fake directory elseif (!($ret = parent::url_stat($url,$flags,$eacl_check)) && $eacl_check) { - list(,/*$apps*/,/*$app*/,$id,$rel_path) = explode('/', egw_vfs::parse_url($url, PHP_URL_PATH), 5); + list(,/*$apps*/,/*$app*/,$id,$rel_path) = explode('/', Vfs::parse_url($url, PHP_URL_PATH), 5); if ($id && !isset($rel_path)) { $ret = array( @@ -168,7 +176,7 @@ class links_stream_wrapper extends links_stream_wrapper_parent 'ctime' => time(), 'nlink' => 2, // eGW addition to return some extra values - 'mime' => egw_vfs::DIR_MIME_TYPE, + 'mime' => Vfs::DIR_MIME_TYPE, ); } } @@ -182,9 +190,9 @@ class links_stream_wrapper extends links_stream_wrapper_parent * Reimplemented, to NOT call the sqlfs functions, as we dont allow to modify the ACL (defined by the apps) * * @param string $path string with path - * @param int $rights=null rights to set, or null to delete the entry - * @param int/boolean $owner=null owner for whom to set the rights, null for the current user, or false to delete all rights for $path - * @param int $fs_id=null fs_id to use, to not query it again (eg. because it's already deleted) + * @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) @@ -230,22 +238,22 @@ class links_stream_wrapper extends links_stream_wrapper_parent if($path[0] != '/') { - if (strpos($path,'?') !== false) $query = egw_vfs::parse_url($path,PHP_URL_QUERY); - $path = egw_vfs::parse_url($path,PHP_URL_PATH).($query ? '?'.$query : ''); + if (strpos($path,'?') !== false) $query = Vfs::parse_url($path,PHP_URL_QUERY); + $path = Vfs::parse_url($path,PHP_URL_PATH).($query ? '?'.$query : ''); } list(,$apps,$app,$id) = explode('/',$path); $ret = false; - if ($apps == 'apps' && $app && !$id || self::check_extended_acl($path,egw_vfs::WRITABLE)) // app directory itself is allways ok + if ($apps == 'apps' && $app && !$id || self::check_extended_acl($path,Vfs::WRITABLE)) // app directory itself is allways ok { - $current_is_root = egw_vfs::$is_root; egw_vfs::$is_root = true; - $current_user = egw_vfs::$user; egw_vfs::$user = 0; + $current_is_root = Vfs::$is_root; Vfs::$is_root = true; + $current_user = Vfs::$user; Vfs::$user = 0; $ret = parent::mkdir($path,0,$options|STREAM_MKDIR_RECURSIVE); if ($id) parent::chmod($path,0); // no other rights - egw_vfs::$user = $current_user; - egw_vfs::$is_root = $current_is_root; + Vfs::$user = $current_user; + Vfs::$is_root = $current_is_root; } //error_log(__METHOD__."($path,$mode,$options) apps=$apps, app=$app, id=$id: returning $ret"); return $ret; @@ -277,21 +285,20 @@ class links_stream_wrapper extends links_stream_wrapper_parent (list($app) = array_slice(explode('/',$url),-3,1)) && $app === 'addressbook') { list($id) = array_slice(explode('/',$url),-2,1); - $name = md5($url); $ab_vcard = new addressbook_vcal('addressbook','text/vcard'); - if (!($GLOBALS[$name] =& $ab_vcard->getVCard($id))) + if (!($vcard =& $ab_vcard->getVCard($id))) { error_log(__METHOD__."('$url', '$mode', $options) addressbook_vcal::getVCard($id) returned false!"); return false; } //error_log(__METHOD__."('$url', '$mode', $options) addressbook_vcal::getVCard($id) returned ".$GLOBALS[$name]); - require_once(EGW_API_INC.'/class.global_stream_wrapper.inc.php'); - $this->opened_stream = fopen('global://'.$name,'r'); - unset($GLOBALS[$name]); // unset it, so it does not use up memory, once the stream is closed + $this->opened_stream = fopen('php://temp', 'wb'); + fwrite($this->opened_stream, $vcard); + fseek($this->opened_stream, 0, SEEK_SET); return true; } // create not existing entry directories on the fly - if ($mode[0] != 'r' && !parent::url_stat($dir = egw_vfs::dirname($url),0) && self::check_extended_acl($dir,egw_vfs::WRITABLE)) + if ($mode[0] != 'r' && !parent::url_stat($dir = Vfs::dirname($url),0) && self::check_extended_acl($dir,Vfs::WRITABLE)) { self::mkdir($dir,0,STREAM_MKDIR_RECURSIVE); } @@ -303,7 +310,7 @@ class links_stream_wrapper extends links_stream_wrapper_parent * * Reimplemented to give no error, if entry directory does not exist. * - * @param string $path URL that was passed to opendir() and that this object is expected to explore. + * @param string $url URL that was passed to opendir() and that this object is expected to explore. * @param $options * @return booelan */ @@ -316,6 +323,14 @@ class links_stream_wrapper extends links_stream_wrapper_parent } return parent::dir_opendir($url, $options); } + + /** + * Register this stream-wrapper + */ + public static function register() + { + stream_register_wrapper(self::SCHEME, __CLASS__); + } } -stream_register_wrapper(links_stream_wrapper::SCHEME ,'links_stream_wrapper'); +StreamWrapper::register(); diff --git a/api/src/Vfs/Sqlfs/StreamWrapper.php b/api/src/Vfs/Sqlfs/StreamWrapper.php new file mode 100644 index 0000000000..f9976d73d6 --- /dev/null +++ b/api/src/Vfs/Sqlfs/StreamWrapper.php @@ -0,0 +1,1939 @@ + + * @copyright (c) 2008-15 by Ralf Becker + * @version $Id$ + */ + +namespace EGroupware\Api\Vfs\Sqlfs; + +use EGroupware\Api\Vfs; + +// explicitly import old phpgwapi classes used: +use egw_cache; +use mime_magic; +use config; +use translation; +use egw_exception_db; +use egw_exception_wrong_parameter; +use egw_exception_assertion_failed; + + +/** + * EGroupware API: VFS - new DB based VFS stream wrapper + * + * The sqlfs stream wrapper has 2 operation modi: + * - content of files is stored in the filesystem (eGW's files_dir) (default) + * - content of files is stored as BLOB in the DB (can be enabled by mounting sqlfs://...?storage=db) + * please note the current (php5.2.6) problems: + * a) retriving files via streams does NOT work for PDO_mysql (bindColum(,,\PDO::PARAM_LOB) does NOT work, string returned) + * (there's a workaround implemented, but it requires to allocate memory for the whole file!) + * b) uploading/writing files > 1M fail on PDOStatement::execute() (setting \PDO::MYSQL_ATTR_MAX_BUFFER_SIZE does NOT help) + * (not sure if that's a bug in PDO/PDO_mysql or an accepted limitation) + * + * I use the PDO DB interface, as it allows to access BLOB's as streams (avoiding to hold them complete in memory). + * + * The stream wrapper interface is according to the docu on php.net + * + * @link http://www.php.net/manual/en/function.stream-wrapper-register.php + */ +class StreamWrapper implements Vfs\StreamWrapperIface +{ + /** + * Mime type of directories, the old vfs uses 'Directory', while eg. WebDAV uses 'httpd/unix-directory' + */ + const DIR_MIME_TYPE = 'httpd/unix-directory'; + /** + * Mime type for symlinks + */ + const SYMLINK_MIME_TYPE = 'application/x-symlink'; + /** + * Scheme / protocoll used for this stream-wrapper + */ + const SCHEME = 'sqlfs'; + /** + * Does url_stat returns a mime type, or has it to be determined otherwise (string with attribute name) + */ + const STAT_RETURN_MIME_TYPE = 'mime'; + /** + * Our tablename + */ + const TABLE = 'egw_sqlfs'; + /** + * Name of our property table + */ + const PROPS_TABLE = 'egw_sqlfs_props'; + /** + * mode-bits, which have to be set for files + */ + const MODE_FILE = 0100000; + /** + * mode-bits, which have to be set for directories + */ + const MODE_DIR = 040000; + /** + * mode-bits, which have to be set for links + */ + const MODE_LINK = 0120000; + /** + * How much should be logged to the apache error-log + * + * 0 = Nothing + * 1 = only errors + * 2 = all function calls and errors (contains passwords too!) + * 3 = log line numbers in sql statements + */ + const LOG_LEVEL = 1; + + /** + * We store the content in the DB (no versioning) + */ + const STORE2DB = 1; + /** + * We store the content in the filesystem (egw_info/server/files_dir) (no versioning) + */ + const STORE2FS = 2; + /** + * default for operation, change that if you want to test with STORE2DB atm + */ + const DEFAULT_OPERATION = self::STORE2FS; + + /** + * operation mode of the opened file + * + * @var int + */ + protected $operation = self::DEFAULT_OPERATION; + + /** + * optional context param when opening the stream, null if no context passed + * + * @var mixed + */ + var $context; + + /** + * Path off the file opened by stream_open + * + * @var string + */ + protected $opened_path; + /** + * Mode of the file opened by stream_open + * + * @var int + */ + protected $opened_mode; + /** + * Stream of the opened file, either from the DB via PDO or the filesystem + * + * @var resource + */ + protected $opened_stream; + /** + * fs_id of opened file + * + * @var int + */ + protected $opened_fs_id; + /** + * Cache containing stat-infos from previous url_stat calls AND dir_opendir calls + * + * It's values are the columns read from the DB (fs_*), not the ones returned by url_stat! + * + * @var array $path => info-array pairs + */ + static protected $stat_cache = array(); + /** + * Reference to the PDO object we use + * + * @var \PDO + */ + static protected $pdo; + /** + * Array with filenames of dir opened with dir_opendir + * + * @var array + */ + protected $opened_dir; + + /** + * Extra columns added since the intitial introduction of sqlfs + * + * Can be set to empty, so get queries running on old versions of sqlfs, eg. for schema updates + * + * @var string; + */ + static public $extra_columns = ',fs_link'; + + /** + * Clears our stat-cache + * + * Normaly not necessary, as it is automatically cleared/updated, UNLESS Vfs::$user changes! + * + * @param string $path ='/' + */ + public static function clearstatcache($path='/') + { + //error_log(__METHOD__."('$path')"); + unset($path); // not used + + self::$stat_cache = array(); + + egw_cache::setSession(self::EACL_APPNAME, 'extended_acl', self::$extended_acl = null); + } + + /** + * This method is called immediately after your stream object is created. + * + * @param string $url URL that was passed to fopen() and that this object is expected to retrieve + * @param string $mode mode used to open the file, as detailed for fopen() + * @param int $options additional flags set by the streams API (or'ed together): + * - STREAM_USE_PATH If path is relative, search for the resource using the include_path. + * - STREAM_REPORT_ERRORS If this flag is set, you are responsible for raising errors using trigger_error() during opening of the stream. + * If this flag is not set, you should not raise any errors. + * @param string &$opened_path full path of the file/resource, if the open was successfull and STREAM_USE_PATH was set + * @param array $overwrite_new =null if set create new file with values overwriten by the given ones + * @return boolean true if the ressource was opened successful, otherwise false + */ + function stream_open ( $url, $mode, $options, &$opened_path, array $overwrite_new=null ) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url,$mode,$options)"); + + $path = Vfs::parse_url($url,PHP_URL_PATH); + $this->operation = self::url2operation($url); + $dir = Vfs::dirname($url); + + $this->opened_path = $opened_path = $path; + $this->opened_mode = $mode = str_replace('b','',$mode); // we are always binary, like every Linux system + $this->opened_stream = null; + + if (!is_null($overwrite_new) || !($stat = static::url_stat($path,STREAM_URL_STAT_QUIET)) || $mode[0] == 'x') // file not found or file should NOT exist + { + if ($mode[0] == 'r' || // does $mode require the file to exist (r,r+) + $mode[0] == 'x' && $stat || // or file should not exist, but does + !($dir_stat=static::url_stat($dir,STREAM_URL_STAT_QUIET)) || // or parent dir does not exist create it + !Vfs::check_access($dir,Vfs::WRITABLE,$dir_stat)) // or we are not allowed to create it + { + self::_remove_password($url); + if (self::LOG_LEVEL) error_log(__METHOD__."($url,$mode,$options) file does not exist or can not be created!"); + if (($options & STREAM_REPORT_ERRORS)) + { + trigger_error(__METHOD__."($url,$mode,$options) file does not exist or can not be created!",E_USER_WARNING); + } + $this->opened_stream = $this->opened_path = $this->opened_mode = null; + return false; + } + // new file --> create it in the DB + $new_file = true; + $query = 'INSERT INTO '.self::TABLE.' (fs_name,fs_dir,fs_mode,fs_uid,fs_gid,fs_created,fs_modified,fs_creator,fs_mime,fs_size,fs_active'. + ') VALUES (:fs_name,:fs_dir,:fs_mode,:fs_uid,:fs_gid,:fs_created,:fs_modified,:fs_creator,:fs_mime,:fs_size,:fs_active)'; + if (self::LOG_LEVEL > 2) $query = '/* '.__METHOD__.': '.__LINE__.' */ '.$query; + $stmt = self::$pdo->prepare($query); + $values = array( + 'fs_name' => Vfs::basename($path), + 'fs_dir' => $dir_stat['ino'], + // we use the mode of the dir, so files in group dirs stay accessible by all members + 'fs_mode' => $dir_stat['mode'] & 0666, + // for the uid we use the uid of the dir if not 0=root or the current user otherwise + 'fs_uid' => $dir_stat['uid'] ? $dir_stat['uid'] : Vfs::$user, + // we allways use the group of the dir + 'fs_gid' => $dir_stat['gid'], + 'fs_created' => self::_pdo_timestamp(time()), + 'fs_modified' => self::_pdo_timestamp(time()), + 'fs_creator' => Vfs::$user, + 'fs_mime' => 'application/octet-stream', // required NOT NULL! + 'fs_size' => 0, + 'fs_active' => self::_pdo_boolean(true), + ); + if ($overwrite_new) $values = array_merge($values,$overwrite_new); + if (!$stmt->execute($values) || !($this->opened_fs_id = self::$pdo->lastInsertId('egw_sqlfs_fs_id_seq'))) + { + $this->opened_stream = $this->opened_path = $this->opened_mode = null; + if (self::LOG_LEVEL) error_log(__METHOD__."($url,$mode,$options) execute() failed: ".self::$pdo->errorInfo()); + return false; + } + if ($this->operation == self::STORE2DB) + { + // we buffer all write operations in a temporary file, which get's written on close + $this->opened_stream = tmpfile(); + } + // create the hash-dirs, if they not yet exist + elseif(!file_exists($fs_dir=Vfs::dirname(self::_fs_path($this->opened_fs_id)))) + { + $umaskbefore = umask(); + if (self::LOG_LEVEL > 1) error_log(__METHOD__." about to call mkdir for $fs_dir # Present UMASK:".decoct($umaskbefore)." called from:".function_backtrace()); + self::mkdir_recursive($fs_dir,0700,true); + } + } + // check if opend file is a directory + elseif($stat && ($stat['mode'] & self::MODE_DIR) == self::MODE_DIR) + { + if (self::LOG_LEVEL) error_log(__METHOD__."($url,$mode,$options) Is a directory!"); + if (($options & STREAM_REPORT_ERRORS)) + { + trigger_error(__METHOD__."($url,$mode,$options) Is a directory!",E_USER_WARNING); + } + $this->opened_stream = $this->opened_path = $this->opened_mode = null; + return false; + } + else + { + if ($mode == 'r' && !Vfs::check_access($url,Vfs::READABLE ,$stat) ||// we are not allowed to read + $mode != 'r' && !Vfs::check_access($url,Vfs::WRITABLE,$stat)) // or edit it + { + self::_remove_password($url); + $op = $mode == 'r' ? 'read' : 'edited'; + if (self::LOG_LEVEL) error_log(__METHOD__."($url,$mode,$options) file can not be $op!"); + if (($options & STREAM_REPORT_ERRORS)) + { + trigger_error(__METHOD__."($url,$mode,$options) file can not be $op!",E_USER_WARNING); + } + $this->opened_stream = $this->opened_path = $this->opened_mode = null; + return false; + } + $this->opened_fs_id = $stat['ino']; + + if ($this->operation == self::STORE2DB) + { + $stmt = self::$pdo->prepare($sql='SELECT fs_content FROM '.self::TABLE.' WHERE fs_id=?'); + $stmt->execute(array($stat['ino'])); + $stmt->bindColumn(1,$this->opened_stream,\PDO::PARAM_LOB); + $stmt->fetch(\PDO::FETCH_BOUND); + // hack to work around a current php bug (http://bugs.php.net/bug.php?id=40913) + // PDOStatement::bindColumn(,,\PDO::PARAM_LOB) is not working for MySQL, content is returned as string :-( + if (is_string($this->opened_stream)) + { + $tmp = fopen('php://temp', 'wb'); + fwrite($tmp, $this->opened_stream); + fseek($tmp, 0, SEEK_SET); + unset($this->opened_stream); + $this->opened_stream = $tmp; + } + //echo 'gettype($this->opened_stream)='; var_dump($this->opened_stream); + } + } + // do we operate directly on the filesystem --> open file from there + if ($this->operation == self::STORE2FS) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__." fopen (may create a directory? mkdir) ($this->opened_fs_id,$mode,$options)"); + if (!($this->opened_stream = fopen(self::_fs_path($this->opened_fs_id),$mode)) && $new_file) + { + // delete db entry again, if we are not able to open a new(!) file + unset($stmt); + $stmt = self::$pdo->prepare('DELETE FROM '.self::TABLE.' WHERE fs_id=:fs_id'); + $stmt->execute(array('fs_id' => $this->opened_fs_id)); + } + } + if ($mode[0] == 'a') // append modes: a, a+ + { + $this->stream_seek(0,SEEK_END); + } + if (!is_resource($this->opened_stream)) error_log(__METHOD__."($url,$mode,$options) NO stream, returning false!"); + + return is_resource($this->opened_stream); + } + + /** + * This method is called when the stream is closed, using fclose(). + * + * You must release any resources that were locked or allocated by the stream. + */ + function stream_close ( ) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__."()"); + + if (is_null($this->opened_path) || !is_resource($this->opened_stream) || !$this->opened_fs_id) + { + return false; + } + + if ($this->opened_mode != 'r') + { + $this->stream_seek(0,SEEK_END); + + // we need to update the mime-type, size and content (if STORE2DB) + $values = array( + 'fs_size' => $this->stream_tell(), + // todo: analyse the file for the mime-type + 'fs_mime' => mime_magic::filename2mime($this->opened_path), + 'fs_id' => $this->opened_fs_id, + 'fs_modifier' => Vfs::$user, + 'fs_modified' => self::_pdo_timestamp(time()), + ); + + if ($this->operation == self::STORE2FS) + { + $stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_size=:fs_size,fs_mime=:fs_mime,fs_modifier=:fs_modifier,fs_modified=:fs_modified WHERE fs_id=:fs_id'); + if (!($ret = $stmt->execute($values))) + { + error_log(__METHOD__."() execute() failed! errorInfo()=".array2string(self::$pdo->errorInfo())); + } + } + else + { + $stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_size=:fs_size,fs_mime=:fs_mime,fs_modifier=:fs_modifier,fs_modified=:fs_modified,fs_content=:fs_content WHERE fs_id=:fs_id'); + $this->stream_seek(0,SEEK_SET); // rewind to the start + foreach($values as $name => &$value) + { + $stmt->bindParam($name,$value); + } + $stmt->bindParam('fs_content', $this->opened_stream, \PDO::PARAM_LOB); + if (!($ret = $stmt->execute())) + { + error_log(__METHOD__."() execute() failed! errorInfo()=".array2string(self::$pdo->errorInfo())); + } + } + } + else + { + $ret = true; + } + $ret = fclose($this->opened_stream) && $ret; + + unset(self::$stat_cache[$this->opened_path]); + $this->opened_stream = $this->opened_path = $this->opened_mode = $this->opend_fs_id = null; + $this->operation = self::DEFAULT_OPERATION; + + return $ret; + } + + /** + * This method is called in response to fread() and fgets() calls on the stream. + * + * You must return up-to count bytes of data from the current read/write position as a string. + * If there are less than count bytes available, return as many as are available. + * If no more data is available, return either FALSE or an empty string. + * You must also update the read/write position of the stream by the number of bytes that were successfully read. + * + * @param int $count + * @return string/false up to count bytes read or false on EOF + */ + function stream_read ( $count ) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__."($count) pos=$this->opened_pos"); + + if (is_resource($this->opened_stream)) + { + return fread($this->opened_stream,$count); + } + return false; + } + + /** + * This method is called in response to fwrite() calls on the stream. + * + * You should store data into the underlying storage used by your stream. + * If there is not enough room, try to store as many bytes as possible. + * You should return the number of bytes that were successfully stored in the stream, or 0 if none could be stored. + * You must also update the read/write position of the stream by the number of bytes that were successfully written. + * + * @param string $data + * @return integer + */ + function stream_write ( $data ) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__."($data)"); + + if (is_resource($this->opened_stream)) + { + return fwrite($this->opened_stream,$data); + } + return false; + } + + /** + * This method is called in response to feof() calls on the stream. + * + * Important: PHP 5.0 introduced a bug that wasn't fixed until 5.1: the return value has to be the oposite! + * + * if(version_compare(PHP_VERSION,'5.0','>=') && version_compare(PHP_VERSION,'5.1','<')) + * { + * $eof = !$eof; + * } + * + * @return boolean true if the read/write position is at the end of the stream and no more data availible, false otherwise + */ + function stream_eof ( ) + { + if (is_resource($this->opened_stream)) + { + return feof($this->opened_stream); + } + return false; + } + + /** + * This method is called in response to ftell() calls on the stream. + * + * @return integer current read/write position of the stream + */ + function stream_tell ( ) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__."()"); + + if (is_resource($this->opened_stream)) + { + return ftell($this->opened_stream); + } + return false; + } + + /** + * This method is called in response to fseek() calls on the stream. + * + * You should update the read/write position of the stream according to offset and whence. + * See fseek() for more information about these parameters. + * + * @param integer $offset + * @param integer $whence SEEK_SET - 0 - Set position equal to offset bytes + * SEEK_CUR - 1 - Set position to current location plus offset. + * SEEK_END - 2 - Set position to end-of-file plus offset. (To move to a position before the end-of-file, you need to pass a negative value in offset.) + * @return boolean TRUE if the position was updated, FALSE otherwise. + */ + function stream_seek ( $offset, $whence ) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__."($offset,$whence)"); + + if (is_resource($this->opened_stream)) + { + return !fseek($this->opened_stream,$offset,$whence); // fseek returns 0 on success and -1 on failure + } + return false; + } + + /** + * This method is called in response to fflush() calls on the stream. + * + * If you have cached data in your stream but not yet stored it into the underlying storage, you should do so now. + * + * @return booelan TRUE if the cached data was successfully stored (or if there was no data to store), or FALSE if the data could not be stored. + */ + function stream_flush ( ) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__."()"); + + if (is_resource($this->opened_stream)) + { + return fflush($this->opened_stream); + } + return false; + } + + /** + * This method is called in response to fstat() calls on the stream. + * + * If you plan to use your wrapper in a require_once you need to define stream_stat(). + * If you plan to allow any other tests like is_file()/is_dir(), you have to define url_stat(). + * stream_stat() must define the size of the file, or it will never be included. + * url_stat() must define mode, or is_file()/is_dir()/is_executable(), and any of those functions affected by clearstatcache() simply won't work. + * It's not documented, but directories must be a mode like 040777 (octal), and files a mode like 0100666. + * If you wish the file to be executable, use 7s instead of 6s. + * The last 3 digits are exactly the same thing as what you pass to chmod. + * 040000 defines a directory, and 0100000 defines a file. + * + * @return array containing the same values as appropriate for the stream. + */ + function stream_stat ( ) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__."($this->opened_path)"); + + return $this->url_stat($this->opened_path,0); + } + + /** + * This method is called in response to unlink() calls on URL paths associated with the wrapper. + * + * It should attempt to delete the item specified by path. + * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support unlinking! + * + * @param string $url + * @return boolean TRUE on success or FALSE on failure + */ + static function unlink ( $url, $parent_stat=null ) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url)"); + + $path = Vfs::parse_url($url,PHP_URL_PATH); + + if (!($stat = self::url_stat($path,STREAM_URL_STAT_LINK)) || !Vfs::check_access(Vfs::dirname($path),Vfs::WRITABLE, $parent_stat)) + { + self::_remove_password($url); + if (self::LOG_LEVEL) error_log(__METHOD__."($url) permission denied!"); + return false; // no permission or file does not exist + } + if ($stat['mime'] == self::DIR_MIME_TYPE) + { + self::_remove_password($url); + if (self::LOG_LEVEL) error_log(__METHOD__."($url) is NO file!"); + return false; // no permission or file does not exist + } + $stmt = self::$pdo->prepare('DELETE FROM '.self::TABLE.' WHERE fs_id=:fs_id'); + unset(self::$stat_cache[$path]); + + if (($ret = $stmt->execute(array('fs_id' => $stat['ino'])))) + { + if (self::url2operation($url) == self::STORE2FS && + ($stat['mode'] & self::MODE_LINK) != self::MODE_LINK) + { + unlink(self::_fs_path($stat['ino'])); + } + // delete props + unset($stmt); + $stmt = self::$pdo->prepare('DELETE FROM '.self::PROPS_TABLE.' WHERE fs_id=?'); + $stmt->execute(array($stat['ino'])); + } + return $ret; + } + + /** + * This method is called in response to rename() calls on URL paths associated with the wrapper. + * + * It should attempt to rename the item specified by path_from to the specification given by path_to. + * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support renaming. + * + * The regular filesystem stream-wrapper returns an error, if $url_from and $url_to are not either both files or both dirs! + * + * @param string $url_from + * @param string $url_to + * @return boolean TRUE on success or FALSE on failure + */ + static function rename ( $url_from, $url_to) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url_from,$url_to)"); + + $path_from = Vfs::parse_url($url_from,PHP_URL_PATH); + $from_dir = Vfs::dirname($path_from); + $path_to = Vfs::parse_url($url_to,PHP_URL_PATH); + $to_dir = Vfs::dirname($path_to); + $operation = self::url2operation($url_from); + + // we have to use array($class,'url_stat'), as $class.'::url_stat' requires PHP 5.2.3 and we currently only require 5.2+ + if (!($from_stat = static::url_stat($path_from, 0)) || + !Vfs::check_access($from_dir, Vfs::WRITABLE, $from_dir_stat = static::url_stat($from_dir, 0))) + { + self::_remove_password($url_from); + self::_remove_password($url_to); + if (self::LOG_LEVEL) error_log(__METHOD__."($url_from,$url_to): $path_from permission denied!"); + return false; // no permission or file does not exist + } + if (!Vfs::check_access($to_dir, Vfs::WRITABLE, $to_dir_stat = static::url_stat($to_dir, 0))) + { + self::_remove_password($url_from); + self::_remove_password($url_to); + if (self::LOG_LEVEL) error_log(__METHOD__."($url_from,$url_to): $path_to permission denied!"); + return false; // no permission or parent-dir does not exist + } + // the filesystem stream-wrapper does NOT allow to rename files to directories, as this makes problems + // for our vfs too, we abort here with an error, like the filesystem one does + if (($to_stat = static::url_stat($path_to, 0)) && + ($to_stat['mime'] === self::DIR_MIME_TYPE) !== ($from_stat['mime'] === self::DIR_MIME_TYPE)) + { + self::_remove_password($url_from); + self::_remove_password($url_to); + $is_dir = $to_stat['mime'] === self::DIR_MIME_TYPE ? 'a' : 'no'; + if (self::LOG_LEVEL) error_log(__METHOD__."($url_to,$url_from) $path_to is $is_dir directory!"); + return false; // no permission or file does not exist + } + // if destination file already exists, delete it + if ($to_stat && !static::unlink($url_to,$operation)) + { + self::_remove_password($url_to); + if (self::LOG_LEVEL) error_log(__METHOD__."($url_to,$url_from) can't unlink existing $url_to!"); + return false; + } + unset(self::$stat_cache[$path_from]); + unset(self::$stat_cache[$path_to]); + + $stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_dir=:fs_dir,fs_name=:fs_name WHERE fs_dir=:old_dir AND fs_name=:old_name'); + $ok = $stmt->execute(array( + 'fs_dir' => $to_dir_stat['ino'], + 'fs_name' => Vfs::basename($path_to), + 'old_dir' => $from_dir_stat['ino'], + 'old_name' => $from_stat['name'], + )); + unset($stmt); + + // check if extension changed and update mime-type in that case (as we currently determine mime-type by it's extension!) + // fixes eg. problems with MsWord storing file with .tmp extension and then renaming to .doc + if ($ok && ($new_mime = Vfs::mime_content_type($url_to,true)) != Vfs::mime_content_type($url_to)) + { + //echo "

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 + */ + private static function mkdir_recursive($pathname, $mode, $depth=0) + { + $maxdepth=10; + $depth2propagate = (int)$depth + 1; + if ($depth2propagate > $maxdepth) return is_dir($pathname); + is_dir(Vfs::dirname($pathname)) || self::mkdir_recursive(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 = 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 = Vfs::dirname($path); + if (($query = 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 || !Vfs::check_access($parent_path,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' => 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' => 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' => 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 = Vfs::parse_url($url,PHP_URL_PATH); + $parent = Vfs::dirname($path); + + if (!($stat = self::url_stat($path,0)) || $stat['mime'] != self::DIR_MIME_TYPE || + !Vfs::check_access($parent,Vfs::WRITABLE)) + { + 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 = 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' => 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 = 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 (!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 = 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 (!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 = 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 (!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 = 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 + !Vfs::check_access($url,Vfs::EXECUTABLE|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[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 = 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,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 (!Vfs::$is_root && !$eacl_access) + { + if (!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 !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(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)Vfs::$user. + ($memberships ? ' OR (fs_mode & 32)=32 AND fs_gid IN('.implode(',',$memberships).')' : '').')'; + //error_log(__METHOD__."() Vfs::\$user=".array2string(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 = Vfs::dirname($link)) || + !Vfs::check_access($dir,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[Vfs::parse_url($link,PHP_URL_PATH)]); + + return !!$stmt->execute(array( + 'fs_name' => Vfs::basename($link), + 'fs_dir' => $dir_stat['ino'], + 'fs_mode' => ($dir_stat['mode'] & 0666), + 'fs_uid' => $dir_stat['uid'] ? $dir_stat['uid'] : Vfs::$user, + 'fs_gid' => $dir_stat['gid'], + 'fs_created' => self::_pdo_timestamp(time()), + 'fs_modified' => self::_pdo_timestamp(time()), + 'fs_creator' => 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 = 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(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 = 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 (!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 = Vfs::$user; + } + if (is_null($rights) || $owner === false) + { + // delete eacl + if (is_null($owner) || $owner == Vfs::$user || + $owner < 0 && Vfs::$user && in_array($owner,$GLOBALS['egw']->accounts->memberships(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 == Vfs::$user || + $owner < 0 && Vfs::$user && in_array($owner,$GLOBALS['egw']->accounts->memberships(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 = 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 = 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'] : 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 (!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'] = 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=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; + } + + /** + * Register __CLASS__ for self::SCHEMA + */ + public static function register() + { + stream_register_wrapper(self::SCHEME, __CLASS__); + } +} + +StreamWrapper::register(); diff --git a/api/src/Vfs/Sqlfs/Utils.php b/api/src/Vfs/Sqlfs/Utils.php new file mode 100644 index 0000000000..ed8343aac3 --- /dev/null +++ b/api/src/Vfs/Sqlfs/Utils.php @@ -0,0 +1,483 @@ + + * @copyright (c) 2008-15 by Ralf Becker + * @version $Id$ + */ + +namespace EGroupware\Api\Vfs\Sqlfs; + +use EGroupware\Api\Vfs; + +// explicitly import old phpgwapi classes used: +use mime_magic; +use egw_exception_assertion_failed; + +/** + * sqlfs stream wrapper utilities: migration db-fs, fsck + */ +class Utils extends StreamWrapper +{ + /** + * Migrate SQLFS content from DB to filesystem + * + * @param boolean $debug true to echo a message for each copied file + */ + static function migrate_db2fs($debug=false) + { + if (!is_object(self::$pdo)) + { + self::_pdo(); + } + $query = 'SELECT fs_id,fs_name,fs_size,fs_content'. + ' FROM '.self::TABLE.' WHERE fs_content IS NOT NULL'; + + $fs_id = $fs_name = $fs_size = $fs_content = null; + $stmt = self::$pdo->prepare($query); + $stmt->bindColumn(1,$fs_id); + $stmt->bindColumn(2,$fs_name); + $stmt->bindColumn(3,$fs_size); + $stmt->bindColumn(4,$fs_content,\PDO::PARAM_LOB); + + if ($stmt->execute()) + { + $n = 0; + foreach($stmt as $row) + { + // hack to work around a current php bug (http://bugs.php.net/bug.php?id=40913) + // PDOStatement::bindColumn(,,\PDO::PARAM_LOB) is not working for MySQL, content is returned as string :-( + if (is_string($fs_content)) + { + $content = fopen('php://temp', 'wb'); + fwrite($content, $fs_content); + fseek($content, 0, SEEK_SET); + unset($fs_content); + } + else + { + $content = $fs_content; + } + if (!is_resource($content)) + { + throw new egw_exception_assertion_failed(__METHOD__."(): fs_id=$fs_id ($fs_name, $fs_size bytes) content is NO resource! ".array2string($content)); + } + $filename = self::_fs_path($fs_id); + if (!file_exists($fs_dir=Vfs::dirname($filename))) + { + self::mkdir_recursive($fs_dir,0700,true); + } + if (!($dest = fopen($filename,'w'))) + { + throw new egw_exception_assertion_failed(__METHOD__."(): fopen($filename,'w') failed!"); + } + if (($bytes = stream_copy_to_stream($content,$dest)) != $fs_size) + { + throw new egw_exception_assertion_failed(__METHOD__."(): fs_id=$fs_id ($fs_name) $bytes bytes copied != size of $fs_size bytes!"); + } + if ($debug) echo "$fs_id: $fs_name: $bytes bytes copied to fs\n"; + fclose($dest); + fclose($content); unset($content); + + ++$n; + unset($row); // not used, as we access bound variables + } + unset($stmt); + + if ($n) // delete all content in DB, if there was some AND no error (exception thrown!) + { + $query = 'UPDATE '.self::TABLE.' SET fs_content=NULL WHERE fs_content IS NOT NULL'; + $stmt = self::$pdo->prepare($query); + $stmt->execute(); + } + } + return $n; + } + + /** + * Check and optionaly fix corruption in sqlfs + * + * @param boolean $check_only =true + * @return array with messages / found problems + */ + public static function fsck($check_only=true) + { + if (!is_object(self::$pdo)) + { + self::_pdo(); + } + $msgs = array(); + foreach(array( + self::fsck_fix_required_nodes($check_only), + self::fsck_fix_multiple_active($check_only), + self::fsck_fix_unconnected($check_only), + self::fsck_fix_no_content($check_only), + ) as $check_msgs) + { + if ($check_msgs) $msgs = array_merge($msgs, $check_msgs); + } + + foreach ($GLOBALS['egw']->hooks->process(array( + 'location' => 'fsck', + 'check_only' => $check_only) + ) as $app_msgs) + { + if ($app_msgs) $msgs = array_merge($msgs, $app_msgs); + } + return $msgs; + } + + /** + * Check and optionally create required nodes: /, /home, /apps + * + * @param boolean $check_only =true + * @return array with messages / found problems + */ + private static function fsck_fix_required_nodes($check_only=true) + { + static $dirs = array( + '/' => 1, + '/home' => 2, + '/apps' => 3, + ); + $stmt = $delete_stmt = null; + $msgs = array(); + foreach($dirs as $path => $id) + { + if (!($stat = self::url_stat($path, STREAM_URL_STAT_LINK))) + { + if ($check_only) + { + $msgs[] = lang('Required directory "%1" not found!', $path); + } + else + { + if (!isset($stmt)) + { + $stmt = self::$pdo->prepare('INSERT INTO '.self::TABLE.' (fs_id,fs_name,fs_dir,fs_mode,fs_uid,fs_gid,fs_size,fs_mime,fs_created,fs_modified,fs_creator'. + ') VALUES (:fs_id,:fs_name,:fs_dir,:fs_mode,:fs_uid,:fs_gid,:fs_size,:fs_mime,:fs_created,:fs_modified,:fs_creator)'); + } + try { + $ok = $stmt->execute($data = array( + 'fs_id' => $id, + 'fs_name' => substr($path,1), + 'fs_dir' => $path == '/' ? 0 : $dirs['/'], + 'fs_mode' => 05, + 'fs_uid' => 0, + 'fs_gid' => 0, + 'fs_size' => 0, + 'fs_mime' => 'httpd/unix-directory', + 'fs_created' => self::_pdo_timestamp(time()), + 'fs_modified' => self::_pdo_timestamp(time()), + 'fs_creator' => 0, + )); + } + catch (\PDOException $e) + { + $ok = false; + unset($e); // ignore exception + } + if (!$ok) // can not insert it, try deleting it first + { + if (!isset($delete_stmt)) + { + $delete_stmt = self::$pdo->prepare('DELETE FROM '.self::TABLE.' WHERE fs_id=:fs_id'); + } + try { + $ok = $delete_stmt->execute(array('fs_id' => $id)) && $stmt->execute($data); + } + catch (\PDOException $e) + { + unset($e); // ignore exception + } + } + $msgs[] = $ok ? lang('Required directory "%1" created.', $path) : + lang('Failed to create required directory "%1"!', $path); + } + } + // check if directory is at least world readable and executable (r-x), we allow more but not less + elseif (($stat['mode'] & 05) != 05) + { + if ($check_only) + { + $msgs[] = lang('Required directory "%1" has wrong mode %2 instead of %3!', + $path, Vfs::int2mode($stat['mode']), Vfs::int2mode(05|0x4000)); + } + else + { + $stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_mode=:fs_mode WHERE fs_id=:fs_id'); + if (($ok = $stmt->execute(array( + 'fs_id' => $id, + 'fs_mode' => 05, + )))) + { + $msgs[] = lang('Mode of required directory "%1" changed to %2.', $path, Vfs::int2mode(05|0x4000)); + } + else + { + $msgs[] = lang('Failed to change mode of required directory "%1" to %2!', $path, Vfs::int2mode(05|0x4000)); + } + } + } + } + if (!$check_only && $msgs) + { + global $oProc; + if (!isset($oProc)) $oProc = new schema_proc(); + // PostgreSQL seems to require to update the sequenz, after manually inserting id's + $oProc->UpdateSequence('egw_sqlfs', 'fs_id'); + } + return $msgs; + } + + /** + * Check and optionally remove files without content part in physical filesystem + * + * @param boolean $check_only =true + * @return array with messages / found problems + */ + private static function fsck_fix_no_content($check_only=true) + { + $stmt = null; + $msgs = array(); + foreach(self::$pdo->query('SELECT fs_id FROM '.self::TABLE. + " WHERE fs_mime!='httpd/unix-directory' AND fs_content IS NULL AND fs_link IS NULL") as $row) + { + if (!file_exists($phy_path=self::_fs_path($row['fs_id']))) + { + $path = self::id2path($row['fs_id']); + if ($check_only) + { + $msgs[] = lang('File %1 has no content in physical filesystem %2!', + $path.' (#'.$row['fs_id'].')',$phy_path); + } + else + { + if (!isset($stmt)) + { + $stmt = self::$pdo->prepare('DELETE FROM '.self::TABLE.' WHERE fs_id=:fs_id'); + $stmt_props = self::$pdo->prepare('DELETE FROM '.self::PROPS_TABLE.' WHERE fs_id=:fs_id'); + } + if ($stmt->execute(array('fs_id' => $row['fs_id'])) && + $stmt_props->execute(array('fs_id' => $row['fs_id']))) + { + $msgs[] = lang('File %1 has no content in physical filesystem %2 --> file removed!',$path,$phy_path); + } + else + { + $msgs[] = lang('File %1 has no content in physical filesystem %2 --> failed to remove file!', + $path.' (#'.$row['fs_id'].')',$phy_path); + } + } + } + } + if ($check_only && $msgs) + { + $msgs[] = lang('Files without content in physical filesystem will be removed.'); + } + return $msgs; + } + + /** + * Name of lost+found directory for unconnected nodes + */ + const LOST_N_FOUND = '/lost+found'; + const LOST_N_FOUND_MOD = 070; + const LOST_N_FOUND_GRP = 'Admins'; + + /** + * Check and optionally fix unconnected nodes - parent directory does not (longer) exists: + * + * SELECT fs.* + * FROM egw_sqlfs fs + * LEFT JOIN egw_sqlfs dir ON dir.fs_id=fs.fs_dir + * WHERE fs.fs_id > 1 && dir.fs_id IS NULL + * + * @param boolean $check_only =true + * @return array with messages / found problems + */ + private static function fsck_fix_unconnected($check_only=true) + { + $lostnfound = null; + $msgs = array(); + foreach(self::$pdo->query('SELECT fs.* FROM '.self::TABLE.' fs'. + ' LEFT JOIN '.self::TABLE.' dir ON dir.fs_id=fs.fs_dir'. + ' WHERE fs.fs_id > 1 AND dir.fs_id IS NULL') as $row) + { + if ($check_only) + { + $msgs[] = lang('Found unconnected %1 %2!', + mime_magic::mime2label($row['fs_mime']), + Vfs::decodePath($row['fs_name']).' (#'.$row['fs_id'].')'); + continue; + } + if (!isset($lostnfound)) + { + // check if we already have /lost+found, create it if not + if (!($lostnfound = self::url_stat(self::LOST_N_FOUND, STREAM_URL_STAT_QUIET))) + { + Vfs::$is_root = true; + if (!self::mkdir(self::LOST_N_FOUND, self::LOST_N_FOUND_MOD, 0) || + !(!($admins = $GLOBALS['egw']->accounts->name2id(self::LOST_N_FOUND_GRP)) || + self::chgrp(self::LOST_N_FOUND, $admins) && self::chmod(self::LOST_N_FOUND,self::LOST_N_FOUND_MOD)) || + !($lostnfound = self::url_stat(self::LOST_N_FOUND, STREAM_URL_STAT_QUIET))) + { + $msgs[] = lang("Can't create directory %1 to connect found unconnected nodes to it!",self::LOST_N_FOUND); + } + else + { + $msgs[] = lang('Successful created new directory %1 for unconnected nods.',self::LOST_N_FOUND); + } + Vfs::$is_root = false; + if (!$lostnfound) break; + } + $stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_dir=:fs_dir WHERE fs_id=:fs_id'); + } + if ($stmt->execute(array( + 'fs_dir' => $lostnfound['ino'], + 'fs_id' => $row['fs_id'], + ))) + { + $msgs[] = lang('Moved unconnected %1 %2 to %3.', + mime_magic::mime2label($row['fs_mime']), + Vfs::decodePath($row['fs_name']).' (#'.$row['fs_id'].')', + self::LOST_N_FOUND); + } + else + { + $msgs[] = lang('Failed to move unconnected %1 %2 to %3!', + mime_magic::mime2label($row['fs_mime']), Vfs::decodePath($row['fs_name']), self::LOST_N_FOUND); + } + } + if ($check_only && $msgs) + { + $msgs[] = lang('Unconnected nodes will be moved to %1.',self::LOST_N_FOUND); + } + return $msgs; + } + + /** + * Check and optionally fix multiple active files and directories with identical path + * + * @param boolean $check_only =true + * @return array with messages / found problems + */ + private static function fsck_fix_multiple_active($check_only=true) + { + $stmt = $inactivate_msg_added = null; + $msgs = array(); + foreach(self::$pdo->query('SELECT fs_dir,fs_name,COUNT(*) FROM '.self::TABLE. + ' WHERE fs_active='.self::_pdo_boolean(true). + ' GROUP BY fs_dir,'.(self::$pdo_type == 'mysql' ? 'BINARY ' : '').'fs_name'. // fs_name is casesensitive! + ' HAVING COUNT(*) > 1') as $row) + { + if (!isset($stmt)) + { + $stmt = self::$pdo->prepare('SELECT *,(SELECT COUNT(*) FROM '.self::TABLE.' sub WHERE sub.fs_dir=fs.fs_id) AS children'. + ' FROM '.self::TABLE.' fs'. + ' WHERE fs.fs_dir=:fs_dir AND fs.fs_active='.self::_pdo_boolean(true).' AND fs.fs_name'.self::$case_sensitive_equal.':fs_name'. + " ORDER BY fs.fs_mime='httpd/unix-directory' DESC,children DESC,fs.fs_modified DESC"); + $inactivate_stmt = self::$pdo->prepare('UPDATE '.self::TABLE. + ' SET fs_active='.self::_pdo_boolean(false). + ' WHERE fs_dir=:fs_dir AND fs_active='.self::_pdo_boolean(true). + ' AND fs_name'.self::$case_sensitive_equal.':fs_name AND fs_id!=:fs_id'); + } + //$msgs[] = array2string($row); + $cnt = 0; + $stmt->execute(array( + 'fs_dir' => $row['fs_dir'], + 'fs_name' => $row['fs_name'], + )); + foreach($stmt as $n => $entry) + { + if ($entry['fs_mime'] == 'httpd/unix-directory') + { + if (!$n) + { + $dir = $entry; // directory to keep + $msgs[] = lang('%1 directories %2 found!', $row[2], self::id2path($entry['fs_id'])); + if ($check_only) break; + } + else + { + if ($entry['children']) + { + $msgs[] = lang('Moved %1 children from directory fs_id=%2 to %3', + $children = self::$pdo->exec('UPDATE '.self::TABLE.' SET fs_dir='.(int)$dir['fs_id']. + ' WHERE fs_dir='.(int)$entry['fs_id']), + $entry['fs_id'], $dir['fs_id']); + + $dir['children'] += $children; + } + self::$pdo->query('DELETE FROM '.self::TABLE.' WHERE fs_id='.(int)$entry['fs_id']); + $msgs[] = lang('Removed (now) empty directory fs_id=%1',$entry['fs_id']); + } + } + elseif (isset($dir)) // file and directory with same name exist! + { + if (!$check_only) + { + $inactivate_stmt->execute(array( + 'fs_dir' => $row['fs_dir'], + 'fs_name' => $row['fs_name'], + 'fs_id' => $dir['fs_id'], + )); + $cnt = $inactivate_stmt->rowCount(); + } + else + { + $cnt = ucfirst(lang('none of %1', $row[2]-1)); + } + $msgs[] = lang('%1 active file(s) with same name as directory inactivated!',$cnt); + break; + } + else // newest file --> set for all other fs_active=false + { + if (!$check_only) + { + $inactivate_stmt->execute(array( + 'fs_dir' => $row['fs_dir'], + 'fs_name' => $row['fs_name'], + 'fs_id' => $entry['fs_id'], + )); + $cnt = $inactivate_stmt->rowCount(); + } + else + { + $cnt = lang('none of %1', $row[2]-1); + } + $msgs[] = lang('More then one active file %1 found, inactivating %2 older revisions!', + self::id2path($entry['fs_id']), $cnt); + break; + } + } + unset($dir); + if ($cnt && !isset($inactivate_msg_added)) + { + $msgs[] = lang('To examine or reinstate inactived files, you might need to turn versioning on.'); + $inactivate_msg_added = true; + } + } + return $msgs; + } +} + +// fsck testcode, if this file is called via it's URL (you need to uncomment it!) +/*if (isset($_SERVER['SCRIPT_FILENAME']) && $_SERVER['SCRIPT_FILENAME'] == __FILE__) +{ + $GLOBALS['egw_info'] = array( + 'flags' => array( + 'currentapp' => 'admin', + 'nonavbar' => true, + ), + ); + include_once '../../header.inc.php'; + + $msgs = Utils::fsck(!isset($_GET['check_only']) || $_GET['check_only']); + echo '

'.implode("

\n

", (array)$msgs)."

\n"; +}*/ \ No newline at end of file diff --git a/phpgwapi/inc/class.vfs_stream_wrapper.inc.php b/api/src/Vfs/StreamWrapper.php similarity index 95% rename from phpgwapi/inc/class.vfs_stream_wrapper.inc.php rename to api/src/Vfs/StreamWrapper.php index 6aae8f2ab8..f4864904f8 100644 --- a/phpgwapi/inc/class.vfs_stream_wrapper.inc.php +++ b/api/src/Vfs/StreamWrapper.php @@ -1,16 +1,24 @@ - * @copyright (c) 2008-14 by Ralf Becker + * @copyright (c) 2008-15 by Ralf Becker * @version $Id$ */ +namespace EGroupware\Api\Vfs; + +use EGroupware\Api\Vfs; + +// explicitly import old phpgwapi classes used: +use mime_magic; +use egw_exception_db; + /** * eGroupWare API: VFS - stream wrapper interface * @@ -19,7 +27,7 @@ * * @link http://www.php.net/manual/en/function.stream-wrapper-register.php */ -class vfs_stream_wrapper implements iface_stream_wrapper +class StreamWrapper implements StreamWrapperIface { /** * Scheme / protocol used for this stream-wrapper @@ -224,7 +232,7 @@ class vfs_stream_wrapper implements iface_stream_wrapper { self::load_wrapper($scheme); } - $url = egw_vfs::concat($url,substr($parts['path'],strlen($mounted))); + $url = Vfs::concat($url,substr($parts['path'],strlen($mounted))); if ($replace_user_pass_host) { @@ -703,7 +711,7 @@ class vfs_stream_wrapper implements iface_stream_wrapper * Requires owner or root rights! * * @param string $path - * @param string $mode mode string see egw_vfs::mode2int + * @param string $mode mode string see Vfs::mode2int * @return boolean true on success, false otherwise */ static function chmod($path,$mode) @@ -845,15 +853,15 @@ class vfs_stream_wrapper implements iface_stream_wrapper if (self::LOG_LEVEL > 0) error_log(__METHOD__."( $path,$options) opendir($this->opened_dir_url) failed!"); return false; } - $this->opened_dir_writable = egw_vfs::check_access($this->opened_dir_url,egw_vfs::WRITABLE); + $this->opened_dir_writable = Vfs::check_access($this->opened_dir_url,Vfs::WRITABLE); // check our fstab if we need to add some of the mountpoints $basepath = self::parse_url($path,PHP_URL_PATH); foreach(array_keys(self::$fstab) as $mounted) { - if (((egw_vfs::dirname($mounted) == $basepath || egw_vfs::dirname($mounted).'/' == $basepath) && $mounted != '/') && + if (((Vfs::dirname($mounted) == $basepath || Vfs::dirname($mounted).'/' == $basepath) && $mounted != '/') && // only return children readable by the user, if dir is not writable (!self::HIDE_UNREADABLES || $this->opened_dir_writable || - egw_vfs::check_access($mounted,egw_vfs::READABLE))) + Vfs::check_access($mounted,Vfs::READABLE))) { $this->extra_dirs[] = basename($mounted); } @@ -918,9 +926,9 @@ class vfs_stream_wrapper implements iface_stream_wrapper { if ($lpath[0] != '/') // concat relative path { - $lpath = egw_vfs::concat(self::parse_url($path,PHP_URL_PATH),'../'.$lpath); + $lpath = Vfs::concat(self::parse_url($path,PHP_URL_PATH),'../'.$lpath); } - $url = egw_vfs::PREFIX.$lpath; + $url = Vfs::PREFIX.$lpath; if (self::LOG_LEVEL > 1) error_log(__METHOD__."($path,$flags) symlif (substr($path,-1) == '/' && $path != '/') $path = substr($path,0,-1); // remove trailing slash eg. added by WebDAVink found and resolved to $url"); // try reading the stat of the link if (($stat = self::url_stat($lpath, STREAM_URL_STAT_QUIET, false, true, $check_symlink_depth-1))) @@ -934,19 +942,19 @@ class vfs_stream_wrapper implements iface_stream_wrapper } catch (egw_exception_db $e) { // some long running operations, eg. merge-print, run into situation that DB closes our separate sqlfs connection - // we try now to reconnect sqlfs_stream_wrapper once + // we try now to reconnect Vfs\Sqlfs\StreamWrapper once // it's done here in vfs_stream_wrapper as situation can happen in sqlfs, links, stylite.links or stylite.versioning if ($try_reconnect) { // reconnect to db - sqlfs_stream_wrapper::reconnect(); + Vfs\Sqlfs\StreamWrapper::reconnect(); return self::url_stat($path, $flags, $try_create_home, $check_symlink_components, $check_symlink_depth, false); } // if numer of tries is exceeded, re-throw exception throw $e; } // check if a failed url_stat was for a home dir, in that case silently create it - if (!$stat && $try_create_home && egw_vfs::dirname(self::parse_url($path,PHP_URL_PATH)) == '/home' && + if (!$stat && $try_create_home && Vfs::dirname(self::parse_url($path,PHP_URL_PATH)) == '/home' && ($id = $GLOBALS['egw']->accounts->name2id(basename($path))) && $GLOBALS['egw']->accounts->id2name($id) == basename($path)) // make sure path has the right case! { @@ -979,8 +987,8 @@ class vfs_stream_wrapper implements iface_stream_wrapper /* Todo: if we hide non readables, we should return false on url_stat for consitency (if dir is not writabel) // Problem: this does NOT stop (calles itself infinit recursive)! - if (self::HIDE_UNREADABLES && !egw_vfs::check_access($path,egw_vfs::READABLE,$stat) && - !egw_vfs::check_access(egw_vfs::dirname($path,egw_vfs::WRITABLE))) + if (self::HIDE_UNREADABLES && !Vfs::check_access($path,Vfs::READABLE,$stat) && + !Vfs::check_access(Vfs::dirname($path,Vfs::WRITABLE))) { return false; } @@ -1004,8 +1012,8 @@ class vfs_stream_wrapper implements iface_stream_wrapper } if (self::LOG_LEVEL > 1) error_log(__METHOD__."('$path',$flags,'$url'): ".function_backtrace(1)); - while (($rel_path = egw_vfs::basename($url).($rel_path ? '/'.$rel_path : '')) && - ($url = egw_vfs::dirname($url))) + while (($rel_path = Vfs::basename($url).($rel_path ? '/'.$rel_path : '')) && + ($url = Vfs::dirname($url))) { if (($stat = self::url_stat($url,0,false,false))) { @@ -1015,14 +1023,14 @@ class vfs_stream_wrapper implements iface_stream_wrapper if ($lpath[0] != '/') { - $lpath = egw_vfs::concat(self::parse_url($url,PHP_URL_PATH),'../'.$lpath); + $lpath = Vfs::concat(self::parse_url($url,PHP_URL_PATH),'../'.$lpath); } - //self::symlinkCache_add($path,egw_vfs::PREFIX.$lpath); - $url = egw_vfs::PREFIX.egw_vfs::concat($lpath,$rel_path); + //self::symlinkCache_add($path,Vfs::PREFIX.$lpath); + $url = Vfs::PREFIX.Vfs::concat($lpath,$rel_path); if (self::LOG_LEVEL > 1) error_log("$log --> lpath='$lpath', url='$url'"); return self::url_stat($url,$flags); } - $url = egw_vfs::concat($url,$rel_path); + $url = Vfs::concat($url,$rel_path); if (self::LOG_LEVEL > 1) error_log(__METHOD__."('$path',$flags,'$url') returning null"); return null; } @@ -1110,10 +1118,10 @@ class vfs_stream_wrapper implements iface_stream_wrapper /** * Clears our internal stat and symlink cache * - * Normaly not necessary, as it is automatically cleared/updated, UNLESS egw_vfs::$user changes! + * Normaly not necessary, as it is automatically cleared/updated, UNLESS Vfs::$user changes! * * We have to clear the symlink cache before AND after calling the backend, - * because auf traversal rights may be different when egw_vfs::$user changes! + * because auf traversal rights may be different when Vfs::$user changes! * * @param string $path ='/' path of backend, whos cache to clear */ @@ -1150,7 +1158,7 @@ class vfs_stream_wrapper implements iface_stream_wrapper while($file !== false && (is_array($this->extra_dirs) && in_array($file,$this->extra_dirs) || // do NOT return extra_dirs twice self::HIDE_UNREADABLES && !$this->opened_dir_writable && - !egw_vfs::check_access(egw_vfs::concat($this->opened_dir_url,$file),egw_vfs::READABLE))); + !Vfs::check_access(Vfs::concat($this->opened_dir_url,$file),Vfs::READABLE))); } if (self::LOG_LEVEL > 1) error_log(__METHOD__."( $this->opened_dir ) = '$file'"); return $file; @@ -1247,7 +1255,16 @@ class vfs_stream_wrapper implements iface_stream_wrapper */ static function scheme2class($scheme) { - return str_replace('.','_',$scheme).'_stream_wrapper'; + list($app, $app_scheme) = explode('.', $scheme); + foreach(array( + empty($app_scheme) ? 'EGroupware\\Api\\Vfs\\'.ucfirst($scheme).'\\StreamWrapper' : // streamwrapper in Api\Vfs + 'EGroupware\\'.ucfirst($app).'\\Vfs\\'.ucfirst($app_scheme).'\\StreamWrapper', // streamwrapper in $app\Vfs + str_replace('.','_',$scheme).'_stream_wrapper', // old (flat) name + ) as $class) + { + //error_log(__METHOD__."('$scheme') class_exists('$class')=".array2string(class_exists($class))); + if (class_exists($class)) return $class; + } } /** @@ -1360,4 +1377,4 @@ class vfs_stream_wrapper implements iface_stream_wrapper } } -vfs_stream_wrapper::init_static(); +StreamWrapper::init_static(); diff --git a/api/src/Vfs/StreamWrapperIface.php b/api/src/Vfs/StreamWrapperIface.php new file mode 100644 index 0000000000..e9ac4e05b9 --- /dev/null +++ b/api/src/Vfs/StreamWrapperIface.php @@ -0,0 +1,250 @@ +=') && version_compare(PHP_VERSION,'5.1','<')) + * { + * $eof = !$eof; + * } + * + * @return boolean true if the read/write position is at the end of the stream and no more data availible, false otherwise + */ + function stream_eof ( ); + + /** + * This method is called in response to ftell() calls on the stream. + * + * @return integer current read/write position of the stream + */ + function stream_tell ( ); + + /** + * This method is called in response to fseek() calls on the stream. + * + * You should update the read/write position of the stream according to offset and whence. + * See fseek() for more information about these parameters. + * + * @param integer $offset + * @param integer $whence SEEK_SET - Set position equal to offset bytes + * SEEK_CUR - Set position to current location plus offset. + * SEEK_END - Set position to end-of-file plus offset. (To move to a position before the end-of-file, you need to pass a negative value in offset.) + * @return boolean TRUE if the position was updated, FALSE otherwise. + */ + function stream_seek ( $offset, $whence ); + + /** + * This method is called in response to fflush() calls on the stream. + * + * If you have cached data in your stream but not yet stored it into the underlying storage, you should do so now. + * + * @return booelan TRUE if the cached data was successfully stored (or if there was no data to store), or FALSE if the data could not be stored. + */ + function stream_flush ( ); + + /** + * This method is called in response to fstat() calls on the stream. + * + * If you plan to use your wrapper in a require_once you need to define stream_stat(). + * If you plan to allow any other tests like is_file()/is_dir(), you have to define url_stat(). + * stream_stat() must define the size of the file, or it will never be included. + * url_stat() must define mode, or is_file()/is_dir()/is_executable(), and any of those functions affected by clearstatcache() simply won't work. + * It's not documented, but directories must be a mode like 040777 (octal), and files a mode like 0100666. + * If you wish the file to be executable, use 7s instead of 6s. + * The last 3 digits are exactly the same thing as what you pass to chmod. + * 040000 defines a directory, and 0100000 defines a file. + * + * @return array containing the same values as appropriate for the stream. + */ + function stream_stat ( ); + + /** + * This method is called in response to unlink() calls on URL paths associated with the wrapper. + * + * It should attempt to delete the item specified by path. + * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support unlinking! + * + * @param string $path + * @return boolean TRUE on success or FALSE on failure + */ + static function unlink ( $path ); + + /** + * This method is called in response to rename() calls on URL paths associated with the wrapper. + * + * It should attempt to rename the item specified by path_from to the specification given by path_to. + * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support renaming. + * + * The regular filesystem stream-wrapper returns an error, if $url_from and $url_to are not either both files or both dirs! + * + * @param string $path_from + * @param string $path_to + * @return boolean TRUE on success or FALSE on failure + */ + static function rename ( $path_from, $path_to ); + + /** + * This method is called in response to mkdir() calls on URL paths associated with the wrapper. + * + * It should attempt to create the directory specified by path. + * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support creating directories. + * + * @param string $path + * @param int $mode + * @param int $options Posible values include STREAM_REPORT_ERRORS and STREAM_MKDIR_RECURSIVE + * @return boolean TRUE on success or FALSE on failure + */ + static function mkdir ( $path, $mode, $options ); + + /** + * This method is called in response to rmdir() calls on URL paths associated with the wrapper. + * + * It should attempt to remove the directory specified by path. + * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support removing directories. + * + * @param string $path + * @param int $options Possible values include STREAM_REPORT_ERRORS. + * @return boolean TRUE on success or FALSE on failure. + */ + static function rmdir ( $path, $options ); + + /** + * This method is called immediately when your stream object is created for examining directory contents with opendir(). + * + * @param string $path URL that was passed to opendir() and that this object is expected to explore. + * @return booelan + */ + function dir_opendir ( $path, $options ); + + /** + * This method is called in response to stat() calls on the URL paths associated with the wrapper. + * + * It should return as many elements in common with the system function as possible. + * Unknown or unavailable values should be set to a rational value (usually 0). + * + * If you plan to use your wrapper in a require_once you need to define stream_stat(). + * If you plan to allow any other tests like is_file()/is_dir(), you have to define url_stat(). + * stream_stat() must define the size of the file, or it will never be included. + * url_stat() must define mode, or is_file()/is_dir()/is_executable(), and any of those functions affected by clearstatcache() simply won't work. + * It's not documented, but directories must be a mode like 040777 (octal), and files a mode like 0100666. + * If you wish the file to be executable, use 7s instead of 6s. + * The last 3 digits are exactly the same thing as what you pass to chmod. + * 040000 defines a directory, and 0100000 defines a file. + * + * @param string $path + * @param int $flags holds additional flags set by the streams API. It can hold one or more of the following values OR'd together: + * - STREAM_URL_STAT_LINK For resources with the ability to link to other resource (such as an HTTP Location: forward, + * or a filesystem symlink). This flag specified that only information about the link itself should be returned, + * not the resource pointed to by the link. + * This flag is set in response to calls to lstat(), is_link(), or filetype(). + * - STREAM_URL_STAT_QUIET If this flag is set, your wrapper should not raise any errors. If this flag is not set, + * you are responsible for reporting errors using the trigger_error() function during stating of the path. + * stat triggers it's own warning anyway, so it makes no sense to trigger one by our stream-wrapper! + * @return array + */ + static function url_stat ( $path, $flags ); + + /** + * This method is called in response to readdir(). + * + * It should return a string representing the next filename in the location opened by dir_opendir(). + * + * @return string + */ + function dir_readdir ( ); + + /** + * This method is called in response to rewinddir(). + * + * It should reset the output generated by dir_readdir(). i.e.: + * The next call to dir_readdir() should return the first entry in the location returned by dir_opendir(). + * + * @return boolean + */ + function dir_rewinddir ( ); + + /** + * This method is called in response to closedir(). + * + * You should release any resources which were locked or allocated during the opening and use of the directory stream. + * + * @return boolean + */ + function dir_closedir ( ); +} diff --git a/etemplate/inc/class.etemplate_new.inc.php b/etemplate/inc/class.etemplate_new.inc.php index f907de8cf5..74e08e3ddd 100644 --- a/etemplate/inc/class.etemplate_new.inc.php +++ b/etemplate/inc/class.etemplate_new.inc.php @@ -624,7 +624,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 9135c9d0ca..a2af4f07b9 100644 --- a/phpgwapi/inc/class.egw_vfs.inc.php +++ b/phpgwapi/inc/class.egw_vfs.inc.php @@ -11,2090 +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']; - } - unset($api_config); - } - if (is_null($url) || is_null($path)) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__.'('.array2string($url).','.array2string($path).') returns '.array2string(self::$fstab)); - return self::$fstab; - } - if (!self::$is_root) - { - if (self::LOG_LEVEL > 0) error_log(__METHOD__.'('.array2string($url).','.array2string($path).') permission denied, you are NOT root!'); - return false; // only root can mount - } - if ($clear_fstab) - { - self::$fstab = array(); - } - if (isset(self::$fstab[$path]) && self::$fstab[$path] === $url) - { - if (self::LOG_LEVEL > 0) error_log(__METHOD__.'('.array2string($url).','.array2string($path).') already mounted.'); - return true; // already mounted - } - self::load_wrapper(self::parse_url($url,PHP_URL_SCHEME)); - - if ($check_url && (!file_exists($url) || opendir($url) === false)) - { - if (self::LOG_LEVEL > 0) error_log(__METHOD__.'('.array2string($url).','.array2string($path).') url does NOT exist!'); - return false; // url does not exist - } - self::$fstab[$path] = $url; - - uksort(self::$fstab,create_function('$a,$b','return strlen($a)-strlen($b);')); - - if ($persitent_mount) - { - config::save_value('vfs_fstab',self::$fstab,'phpgwapi'); - $GLOBALS['egw_info']['server']['vfs_fstab'] = self::$fstab; - // invalidate session cache - if (method_exists($GLOBALS['egw'],'invalidate_session_cache')) // egw object in setup is limited - { - $GLOBALS['egw']->invalidate_session_cache(); - } - } - if (self::LOG_LEVEL > 1) error_log(__METHOD__.'('.array2string($url).','.array2string($path).') returns true (successful new mount).'); - return true; - } - - /** - * Unmounts a filesystem part of the vfs - * - * @param string $path url or path of the filesystem to unmount - */ - static function umount($path) - { - if (!self::$is_root) - { - if (self::LOG_LEVEL > 0) error_log(__METHOD__.'('.array2string($path).','.array2string($path).') permission denied, you are NOT root!'); - return false; // only root can mount - } - if (!isset(self::$fstab[$path]) && ($path = array_search($path,self::$fstab)) === false) - { - if (self::LOG_LEVEL > 0) error_log(__METHOD__.'('.array2string($path).') NOT mounted!'); - return false; // $path not mounted - } - unset(self::$fstab[$path]); - - config::save_value('vfs_fstab',self::$fstab,'phpgwapi'); - $GLOBALS['egw_info']['server']['vfs_fstab'] = self::$fstab; - // invalidate session cache - if (method_exists($GLOBALS['egw'],'invalidate_session_cache')) // egw object in setup is limited - { - $GLOBALS['egw']->invalidate_session_cache(); - } - if (self::LOG_LEVEL > 1) error_log(__METHOD__.'('.array2string($path).') returns true (successful unmount).'); - return true; - } - - /** - * Check if file is hidden: name starts with a '.' or is Thumbs.db - * - * @param string $path - * @return boolean - */ - public static function is_hidden($path) - { - $file = self::basename($path); - - return $file[0] == '.' || $file == 'Thumbs.db'; - } - - /** - * find = recursive search over the filesystem - * - * @param string|array $base base of the search - * @param array $options =null the following keys are allowed: - * - type => {d|f|F} d=dirs, f=files (incl. symlinks), F=files (incl. symlinks to files), default all - * - depth => {true|false(default)} put the contents of a dir before the dir itself - * - dirsontop => {true(default)|false} allways return dirs before the files (two distinct blocks) - * - mindepth,maxdepth minimal or maximal depth to be returned - * - name,path => pattern with *,? wildcards, eg. "*.php" - * - name_preg,path_preg => preg regular expresion, eg. "/(vfs|wrapper)/" - * - uid,user,gid,group,nouser,nogroup file belongs to user/group with given name or (numerical) id - * - mime => type[/subtype] or perl regular expression starting with a "/" eg. "/^(image|video)\\//i" - * - empty,size => (+|-|)N - * - cmin/mmin => (+|-|)N file/dir create/modified in the last N minutes - * - ctime/mtime => (+|-|)N file/dir created/modified in the last N days - * - url => false(default),true allow (and return) full URL's instead of VFS pathes (only set it, if you know what you doing securitywise!) - * - need_mime => false(default),true should we return the mime type - * - order => name order rows by name column - * - sort => (ASC|DESC) sort, default ASC - * - limit => N,[n=0] return N entries from position n on, which defaults to 0 - * - follow => {true|false(default)} follow symlinks - * - hidden => {true|false(default)} include hidden files (name starts with a '.' or is Thumbs.db) - * @param string|array/true $exec =null function to call with each found file/dir as first param and stat array as last param or - * true to return file => stat pairs - * @param array $exec_params =null further params for exec as array, path is always the first param and stat the last! - * @return array of pathes if no $exec, otherwise path => stat pairs - */ - static function find($base,$options=null,$exec=null,$exec_params=null) - { - //error_log(__METHOD__."(".print_r($base,true).",".print_r($options,true).",".print_r($exec,true).",".print_r($exec_params,true).")\n"); - - $type = $options['type']; // 'd', 'f' or 'F' - $dirs_last = $options['depth']; // put content of dirs before the dir itself - // show dirs on top by default, if no recursive listing (allways disabled if $type specified, as unnecessary) - $dirsontop = !$type && (isset($options['dirsontop']) ? (boolean)$options['dirsontop'] : isset($options['maxdepth'])&&$options['maxdepth']>0); - if ($dirsontop) $options['need_mime'] = true; // otherwise dirsontop can NOT work - - // process some of the options (need to be done only once) - if (isset($options['name']) && !isset($options['name_preg'])) // change from simple *,? wildcards to preg regular expression once - { - $options['name_preg'] = '/^'.str_replace(array('\\?','\\*'),array('.{1}','.*'),preg_quote($options['name'])).'$/i'; - } - if (isset($options['path']) && !isset($options['preg_path'])) // change from simple *,? wildcards to preg regular expression once - { - $options['path_preg'] = '/^'.str_replace(array('\\?','\\*'),array('.{1}','.*'),preg_quote($options['path'])).'$/i'; - } - if (!isset($options['uid'])) - { - if (isset($options['user'])) - { - $options['uid'] = $GLOBALS['egw']->accounts->name2id($options['user'],'account_lid','u'); - } - elseif (isset($options['nouser'])) - { - $options['uid'] = 0; - } - } - if (!isset($options['gid'])) - { - if (isset($options['group'])) - { - $options['gid'] = abs($GLOBALS['egw']->accounts->name2id($options['group'],'account_lid','g')); - } - elseif (isset($options['nogroup'])) - { - $options['gid'] = 0; - } - } - if ($options['order'] == 'mime') - { - $options['need_mime'] = true; // we need to return the mime colum - } - $url = $options['url']; - - if (!is_array($base)) - { - $base = array($base); - } - $result = array(); - foreach($base as $path) - { - if (!$url) - { - if ($path[0] != '/' || !egw_vfs::stat($path)) continue; - $path = egw_vfs::PREFIX . $path; - } - if (!isset($options['remove'])) - { - $options['remove'] = count($base) == 1 ? count(explode('/',$path))-3+(int)(substr($path,-1)!='/') : 0; - } - $is_dir = is_dir($path); - if ((int)$options['mindepth'] == 0 && (!$dirs_last || !$is_dir)) - { - self::_check_add($options,$path,$result); - } - if ($is_dir && (!isset($options['maxdepth']) || ($options['maxdepth'] > 0 && $options['depth'] < $options['maxdepth'])) && ($dir = @opendir($path))) - { - while(($fname = readdir($dir)) !== false) - { - if ($fname == '.' || $fname == '..') continue; // ignore current and parent dir! - - if (self::is_hidden($fname) && !$options['hidden']) continue; // ignore hidden files - - $file = self::concat($path, $fname); - - if ((int)$options['mindepth'] <= 1) - { - self::_check_add($options,$file,$result); - } - // only descend into subdirs, if it's a real dir (no link to a dir) or we should follow symlinks - if (is_dir($file) && ($options['follow'] || !is_link($file)) && (!isset($options['maxdepth']) || $options['maxdepth'] > 1)) - { - $opts = $options; - if ($opts['mindepth']) $opts['mindepth']--; - if ($opts['maxdepth']) $opts['depth']++; - unset($opts['order']); - unset($opts['limit']); - foreach(self::find($options['url']?$file:self::parse_url($file,PHP_URL_PATH),$opts,true) as $p => $s) - { - unset($result[$p]); - $result[$p] = $s; - } - } - } - closedir($dir); - } - if ($is_dir && (int)$options['mindepth'] == 0 && $dirs_last) - { - self::_check_add($options,$path,$result); - } - } - // sort code, to place directories before files, if $dirsontop enabled - $dirsfirst = $dirsontop ? '($a[mime]==\''.self::DIR_MIME_TYPE.'\')!==($b[mime]==\''.self::DIR_MIME_TYPE.'\')?'. - '($a[mime]==\''.self::DIR_MIME_TYPE.'\'?-1:1):' : ''; - // ordering of the rows - if (isset($options['order'])) - { - $sort = strtolower($options['sort']) == 'desc' ? '-' : ''; - switch($options['order']) - { - // sort numerical - case 'size': - case 'uid': - case 'gid': - case 'mode': - case 'ctime': - case 'mtime': - $code = $dirsfirst.$sort.'($a[\''.$options['order'].'\']-$b[\''.$options['order'].'\']);'; - // always use name as second sort criteria - $code = '$cmp = '.$code.' return $cmp ? $cmp : strcasecmp($a[\'name\'],$b[\'name\']);'; - $ok = uasort($result,create_function('$a,$b',$code)); - break; - - // sort alphanumerical - default: - $options['order'] = 'name'; - // fall throught - case 'name': - case 'mime': - $code = $dirsfirst.$sort.'strcasecmp($a[\''.$options['order'].'\'],$b[\''.$options['order'].'\']);'; - if ($options['order'] != 'name') - { - // always use name as second sort criteria - $code = '$cmp = '.$code.' return $cmp ? $cmp : strcasecmp($a[\'name\'],$b[\'name\']);'; - } - else - { - $code = 'return '.$code; - } - $ok = uasort($result,create_function('$a,$b',$code)); - break; - } - //echo "

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

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

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

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

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

\n"; - $stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_mime=:fs_mime WHERE fs_id=:fs_id'); - $stmt->execute(array( - 'fs_mime' => $new_mime, - 'fs_id' => $from_stat['ino'], - )); - unset(self::$stat_cache[$path_to]); - } - return $ok; - } - - /** - * due to problems with recursive directory creation, we have our own here - */ - private 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)) - { - 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 16911912a6..5533aac921 100644 --- a/phpgwapi/inc/class.sqlfs_utils.inc.php +++ b/phpgwapi/inc/class.sqlfs_utils.inc.php @@ -7,474 +7,13 @@ * @package api * @subpackage vfs * @author Ralf Becker - * @copyright (c) 2008-14 by Ralf Becker + * @copyright (c) 2008-15 by Ralf Becker * @version $Id$ */ -require_once 'class.iface_stream_wrapper.inc.php'; -require_once 'class.sqlfs_stream_wrapper.inc.php'; +use EGroupware\Api\Vfs\Sqlfs; /** - * sqlfs stream wrapper utilities: migration db-fs, fsck + * @depredated use EGroupware\Api\Vfs\Sqlfs\Utils */ -class sqlfs_utils extends sqlfs_stream_wrapper -{ - /** - * Migrate SQLFS content from DB to filesystem - * - * @param boolean $debug true to echo a message for each copied file - */ - static function migrate_db2fs($debug=false) - { - if (!is_object(self::$pdo)) - { - self::_pdo(); - } - $query = 'SELECT fs_id,fs_name,fs_size,fs_content'. - ' FROM '.self::TABLE.' WHERE fs_content IS NOT NULL'; - - $fs_id = $fs_name = $fs_size = $fs_content = null; - $stmt = self::$pdo->prepare($query); - $stmt->bindColumn(1,$fs_id); - $stmt->bindColumn(2,$fs_name); - $stmt->bindColumn(3,$fs_size); - $stmt->bindColumn(4,$fs_content,PDO::PARAM_LOB); - - if ($stmt->execute()) - { - $n = 0; - foreach($stmt as $row) - { - // hack to work around a current php bug (http://bugs.php.net/bug.php?id=40913) - // PDOStatement::bindColumn(,,PDO::PARAM_LOB) is not working for MySQL, content is returned as string :-( - if (is_string($fs_content)) - { - $name = md5($fs_name.$fs_id); - $GLOBALS[$name] =& $fs_content; - require_once(EGW_API_INC.'/class.global_stream_wrapper.inc.php'); - $content = fopen('global://'.$name,'r'); - if (!$content) echo "fopen('global://$name','w' failed, strlen(\$GLOBALS['$name'])=".strlen($GLOBALS[$name]).", \$GLOBALS['$name']=".substr($GLOBALS['$name'],0,100)."...\n"; - unset($GLOBALS[$name]); // unset it, so it does not use up memory, once the stream is closed - } - else - { - $content = $fs_content; - } - if (!is_resource($content)) - { - throw new egw_exception_assertion_failed(__METHOD__."(): fs_id=$fs_id ($fs_name, $fs_size bytes) content is NO resource! ".array2string($content)); - } - $filename = self::_fs_path($fs_id); - if (!file_exists($fs_dir=egw_vfs::dirname($filename))) - { - self::mkdir_recursive($fs_dir,0700,true); - } - if (!($dest = fopen($filename,'w'))) - { - throw new egw_exception_assertion_failed(__METHOD__."(): fopen($filename,'w') failed!"); - } - if (($bytes = stream_copy_to_stream($content,$dest)) != $fs_size) - { - throw new egw_exception_assertion_failed(__METHOD__."(): fs_id=$fs_id ($fs_name) $bytes bytes copied != size of $fs_size bytes!"); - } - if ($debug) echo "$fs_id: $fs_name: $bytes bytes copied to fs\n"; - fclose($dest); - fclose($content); unset($content); - - ++$n; - unset($row); // not used, as we access bound variables - } - unset($stmt); - - if ($n) // delete all content in DB, if there was some AND no error (exception thrown!) - { - $query = 'UPDATE '.self::TABLE.' SET fs_content=NULL WHERE fs_content IS NOT NULL'; - $stmt = self::$pdo->prepare($query); - $stmt->execute(); - } - } - return $n; - } - - /** - * Check and optionaly fix corruption in sqlfs - * - * @param boolean $check_only=true - * @return array with messages / found problems - */ - public static function fsck($check_only=true) - { - if (!is_object(self::$pdo)) - { - self::_pdo(); - } - $msgs = array(); - foreach(array( - self::fsck_fix_required_nodes($check_only), - self::fsck_fix_multiple_active($check_only), - self::fsck_fix_unconnected($check_only), - self::fsck_fix_no_content($check_only), - ) as $check_msgs) - { - if ($check_msgs) $msgs = array_merge($msgs, $check_msgs); - } - - foreach ($GLOBALS['egw']->hooks->process(array( - 'location' => 'fsck', - 'check_only' => $check_only) - ) as $app_msgs) - { - if ($app_msgs) $msgs = array_merge($msgs, $app_msgs); - } - return $msgs; - } - - /** - * Check and optionally create required nodes: /, /home, /apps - * - * @param boolean $check_only=true - * @return array with messages / found problems - */ - private static function fsck_fix_required_nodes($check_only=true) - { - static $dirs = array( - '/' => 1, - '/home' => 2, - '/apps' => 3, - ); - $stmt = $delete_stmt = null; - $msgs = array(); - foreach($dirs as $path => $id) - { - if (!($stat = self::url_stat($path, STREAM_URL_STAT_LINK))) - { - if ($check_only) - { - $msgs[] = lang('Required directory "%1" not found!', $path); - } - else - { - if (!isset($stmt)) - { - $stmt = self::$pdo->prepare('INSERT INTO '.self::TABLE.' (fs_id,fs_name,fs_dir,fs_mode,fs_uid,fs_gid,fs_size,fs_mime,fs_created,fs_modified,fs_creator'. - ') VALUES (:fs_id,:fs_name,:fs_dir,:fs_mode,:fs_uid,:fs_gid,:fs_size,:fs_mime,:fs_created,:fs_modified,:fs_creator)'); - } - try { - $ok = $stmt->execute($data = array( - 'fs_id' => $id, - 'fs_name' => substr($path,1), - 'fs_dir' => $path == '/' ? 0 : $dirs['/'], - 'fs_mode' => 05, - 'fs_uid' => 0, - 'fs_gid' => 0, - 'fs_size' => 0, - 'fs_mime' => 'httpd/unix-directory', - 'fs_created' => self::_pdo_timestamp(time()), - 'fs_modified' => self::_pdo_timestamp(time()), - 'fs_creator' => 0, - )); - } - catch (PDOException $e) - { - $ok = false; - unset($e); // ignore exception - } - if (!$ok) // can not insert it, try deleting it first - { - if (!isset($delete_stmt)) - { - $delete_stmt = self::$pdo->prepare('DELETE FROM '.self::TABLE.' WHERE fs_id=:fs_id'); - } - try { - $ok = $delete_stmt->execute(array('fs_id' => $id)) && $stmt->execute($data); - } - catch (PDOException $e) - { - unset($e); // ignore exception - } - } - $msgs[] = $ok ? lang('Required directory "%1" created.', $path) : - lang('Failed to create required directory "%1"!', $path); - } - } - // check if directory is at least world readable and executable (r-x), we allow more but not less - elseif (($stat['mode'] & 05) != 05) - { - if ($check_only) - { - $msgs[] = lang('Required directory "%1" has wrong mode %2 instead of %3!', - $path, egw_vfs::int2mode($stat['mode']), egw_vfs::int2mode(05|0x4000)); - } - else - { - $stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_mode=:fs_mode WHERE fs_id=:fs_id'); - if (($ok = $stmt->execute(array( - 'fs_id' => $id, - 'fs_mode' => 05, - )))) - { - $msgs[] = lang('Mode of required directory "%1" changed to %2.', $path, egw_vfs::int2mode(05|0x4000)); - } - else - { - $msgs[] = lang('Failed to change mode of required directory "%1" to %2!', $path, egw_vfs::int2mode(05|0x4000)); - } - } - } - } - if (!$check_only && $msgs) - { - global $oProc; - if (!isset($oProc)) $oProc = new schema_proc(); - // PostgreSQL seems to require to update the sequenz, after manually inserting id's - $oProc->UpdateSequence('egw_sqlfs', 'fs_id'); - } - return $msgs; - } - - /** - * Check and optionally remove files without content part in physical filesystem - * - * @param boolean $check_only=true - * @return array with messages / found problems - */ - private static function fsck_fix_no_content($check_only=true) - { - $stmt = null; - $msgs = array(); - foreach(self::$pdo->query('SELECT fs_id FROM '.self::TABLE. - " WHERE fs_mime!='httpd/unix-directory' AND fs_content IS NULL AND fs_link IS NULL") as $row) - { - if (!file_exists($phy_path=self::_fs_path($row['fs_id']))) - { - $path = self::id2path($row['fs_id']); - if ($check_only) - { - $msgs[] = lang('File %1 has no content in physical filesystem %2!', - $path.' (#'.$row['fs_id'].')',$phy_path); - } - else - { - if (!isset($stmt)) - { - $stmt = self::$pdo->prepare('DELETE FROM '.self::TABLE.' WHERE fs_id=:fs_id'); - $stmt_props = self::$pdo->prepare('DELETE FROM '.self::PROPS_TABLE.' WHERE fs_id=:fs_id'); - } - if ($stmt->execute(array('fs_id' => $row['fs_id'])) && - $stmt_props->execute(array('fs_id' => $row['fs_id']))) - { - $msgs[] = lang('File %1 has no content in physical filesystem %2 --> file removed!',$path,$phy_path); - } - else - { - $msgs[] = lang('File %1 has no content in physical filesystem %2 --> failed to remove file!', - $path.' (#'.$row['fs_id'].')',$phy_path); - } - } - } - } - if ($check_only && $msgs) - { - $msgs[] = lang('Files without content in physical filesystem will be removed.'); - } - return $msgs; - } - - /** - * Name of lost+found directory for unconnected nodes - */ - const LOST_N_FOUND = '/lost+found'; - const LOST_N_FOUND_MOD = 070; - const LOST_N_FOUND_GRP = 'Admins'; - - /** - * Check and optionally fix unconnected nodes - parent directory does not (longer) exists: - * - * SELECT fs.* - * FROM egw_sqlfs fs - * LEFT JOIN egw_sqlfs dir ON dir.fs_id=fs.fs_dir - * WHERE fs.fs_id > 1 && dir.fs_id IS NULL - * - * @param boolean $check_only=true - * @return array with messages / found problems - */ - private static function fsck_fix_unconnected($check_only=true) - { - $lostnfound = null; - $msgs = array(); - foreach(self::$pdo->query('SELECT fs.* FROM '.self::TABLE.' fs'. - ' LEFT JOIN '.self::TABLE.' dir ON dir.fs_id=fs.fs_dir'. - ' WHERE fs.fs_id > 1 AND dir.fs_id IS NULL') as $row) - { - if ($check_only) - { - $msgs[] = lang('Found unconnected %1 %2!', - mime_magic::mime2label($row['fs_mime']), - egw_vfs::decodePath($row['fs_name']).' (#'.$row['fs_id'].')'); - continue; - } - if (!isset($lostnfound)) - { - // check if we already have /lost+found, create it if not - if (!($lostnfound = self::url_stat(self::LOST_N_FOUND, STREAM_URL_STAT_QUIET))) - { - egw_vfs::$is_root = true; - if (!self::mkdir(self::LOST_N_FOUND, self::LOST_N_FOUND_MOD, 0) || - !(!($admins = $GLOBALS['egw']->accounts->name2id(self::LOST_N_FOUND_GRP)) || - self::chgrp(self::LOST_N_FOUND, $admins) && self::chmod(self::LOST_N_FOUND,self::LOST_N_FOUND_MOD)) || - !($lostnfound = self::url_stat(self::LOST_N_FOUND, STREAM_URL_STAT_QUIET))) - { - $msgs[] = lang("Can't create directory %1 to connect found unconnected nodes to it!",self::LOST_N_FOUND); - } - else - { - $msgs[] = lang('Successful created new directory %1 for unconnected nods.',self::LOST_N_FOUND); - } - egw_vfs::$is_root = false; - if (!$lostnfound) break; - } - $stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_dir=:fs_dir WHERE fs_id=:fs_id'); - } - if ($stmt->execute(array( - 'fs_dir' => $lostnfound['ino'], - 'fs_id' => $row['fs_id'], - ))) - { - $msgs[] = lang('Moved unconnected %1 %2 to %3.', - mime_magic::mime2label($row['fs_mime']), - egw_vfs::decodePath($row['fs_name']).' (#'.$row['fs_id'].')', - self::LOST_N_FOUND); - } - else - { - $msgs[] = lang('Failed to move unconnected %1 %2 to %3!', - mime_magic::mime2label($row['fs_mime']), egw_vfs::decodePath($row['fs_name']), self::LOST_N_FOUND); - } - } - if ($check_only && $msgs) - { - $msgs[] = lang('Unconnected nodes will be moved to %1.',self::LOST_N_FOUND); - } - return $msgs; - } - - /** - * Check and optionally fix multiple active files and directories with identical path - * - * @param boolean $check_only=true - * @return array with messages / found problems - */ - private static function fsck_fix_multiple_active($check_only=true) - { - $stmt = $inactivate_msg_added = null; - $msgs = array(); - foreach(self::$pdo->query('SELECT fs_dir,fs_name,COUNT(*) FROM '.self::TABLE. - ' WHERE fs_active='.self::_pdo_boolean(true). - ' GROUP BY fs_dir,'.(self::$pdo_type == 'mysql' ? 'BINARY ' : '').'fs_name'. // fs_name is casesensitive! - ' HAVING COUNT(*) > 1') as $row) - { - if (!isset($stmt)) - { - $stmt = self::$pdo->prepare('SELECT *,(SELECT COUNT(*) FROM '.self::TABLE.' sub WHERE sub.fs_dir=fs.fs_id) AS children'. - ' FROM '.self::TABLE.' fs'. - ' WHERE fs.fs_dir=:fs_dir AND fs.fs_active='.self::_pdo_boolean(true).' AND fs.fs_name'.self::$case_sensitive_equal.':fs_name'. - " ORDER BY fs.fs_mime='httpd/unix-directory' DESC,children DESC,fs.fs_modified DESC"); - $inactivate_stmt = self::$pdo->prepare('UPDATE '.self::TABLE. - ' SET fs_active='.self::_pdo_boolean(false). - ' WHERE fs_dir=:fs_dir AND fs_active='.self::_pdo_boolean(true). - ' AND fs_name'.self::$case_sensitive_equal.':fs_name AND fs_id!=:fs_id'); - } - //$msgs[] = array2string($row); - $cnt = 0; - $stmt->execute(array( - 'fs_dir' => $row['fs_dir'], - 'fs_name' => $row['fs_name'], - )); - foreach($stmt as $n => $entry) - { - if ($entry['fs_mime'] == 'httpd/unix-directory') - { - if (!$n) - { - $dir = $entry; // directory to keep - $msgs[] = lang('%1 directories %2 found!', $row[2], self::id2path($entry['fs_id'])); - if ($check_only) break; - } - else - { - if ($entry['children']) - { - $msgs[] = lang('Moved %1 children from directory fs_id=%2 to %3', - $children = self::$pdo->exec('UPDATE '.self::TABLE.' SET fs_dir='.(int)$dir['fs_id']. - ' WHERE fs_dir='.(int)$entry['fs_id']), - $entry['fs_id'], $dir['fs_id']); - - $dir['children'] += $children; - } - self::$pdo->query('DELETE FROM '.self::TABLE.' WHERE fs_id='.(int)$entry['fs_id']); - $msgs[] = lang('Removed (now) empty directory fs_id=%1',$entry['fs_id']); - } - } - elseif (isset($dir)) // file and directory with same name exist! - { - if (!$check_only) - { - $inactivate_stmt->execute(array( - 'fs_dir' => $row['fs_dir'], - 'fs_name' => $row['fs_name'], - 'fs_id' => $dir['fs_id'], - )); - $cnt = $inactivate_stmt->rowCount(); - } - else - { - $cnt = ucfirst(lang('none of %1', $row[2]-1)); - } - $msgs[] = lang('%1 active file(s) with same name as directory inactivated!',$cnt); - break; - } - else // newest file --> set for all other fs_active=false - { - if (!$check_only) - { - $inactivate_stmt->execute(array( - 'fs_dir' => $row['fs_dir'], - 'fs_name' => $row['fs_name'], - 'fs_id' => $entry['fs_id'], - )); - $cnt = $inactivate_stmt->rowCount(); - } - else - { - $cnt = lang('none of %1', $row[2]-1); - } - $msgs[] = lang('More then one active file %1 found, inactivating %2 older revisions!', - self::id2path($entry['fs_id']), $cnt); - break; - } - } - unset($dir); - if ($cnt && !isset($inactivate_msg_added)) - { - $msgs[] = lang('To examine or reinstate inactived files, you might need to turn versioning on.'); - $inactivate_msg_added = true; - } - } - return $msgs; - } -} - -// fsck testcode, if this file is called via it's URL (you need to uncomment it!) -/*if (isset($_SERVER['SCRIPT_FILENAME']) && $_SERVER['SCRIPT_FILENAME'] == __FILE__) -{ - $GLOBALS['egw_info'] = array( - 'flags' => array( - 'currentapp' => 'admin', - 'nonavbar' => true, - ), - ); - include_once '../../header.inc.php'; - - $msgs = sqlfs_utils::fsck(!isset($_GET['check_only']) || $_GET['check_only']); - echo '

'.implode("

\n

", (array)$msgs)."

\n"; -}*/ \ No newline at end of file +class sqlfs_utils extends Sqlfs\Utils {} diff --git a/phpgwapi/inc/common_functions.inc.php b/phpgwapi/inc/common_functions.inc.php index 92dea212c1..f8d855cfd5 100755 --- a/phpgwapi/inc/common_functions.inc.php +++ b/phpgwapi/inc/common_functions.inc.php @@ -1616,16 +1616,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; @@ -1660,9 +1688,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'))