diff --git a/api/src/Link/Sharing.php b/api/src/Link/Sharing.php index 1e50cd0fa2..1005e369f7 100644 --- a/api/src/Link/Sharing.php +++ b/api/src/Link/Sharing.php @@ -57,9 +57,9 @@ class Sharing extends \EGroupware\Api\Sharing * The anonymous user probably doesn't have the needed permissions to access * the record, so we should set that up to avoid permission errors */ - protected function after_login() + protected static function after_login(array $share) { - list($app) = explode('::', $this->share['share_path']); + list($app) = explode('::', $share['share_path']); // allow app (gets overwritten by session::create) $GLOBALS['egw_info']['flags']['currentapp'] = $app; diff --git a/api/src/Session.php b/api/src/Session.php index 5993ed481f..44c0f9713e 100644 --- a/api/src/Session.php +++ b/api/src/Session.php @@ -2019,10 +2019,11 @@ class Session /** * Initialise the used session handler * + * @param string? $sessionid =null default use self::get_sessionid() * @return boolean true if we have a session, false otherwise * @throws \ErrorException if there is no PHP session support */ - public static function init_handler() + public static function init_handler($sessionid=null) { switch(session_status()) { @@ -2032,7 +2033,7 @@ class Session if (headers_sent()) return false; // only gives warnings ini_set('session.use_cookies',0); // disable the automatic use of cookies, as it uses the path / by default session_name(self::EGW_SESSION_NAME); - if (($sessionid = self::get_sessionid())) + if (isset($sessionid) || ($sessionid = self::get_sessionid())) { session_id($sessionid); self::cache_control(); diff --git a/api/src/Sharing.php b/api/src/Sharing.php index a71921cb0e..496b2ed928 100644 --- a/api/src/Sharing.php +++ b/api/src/Sharing.php @@ -95,16 +95,16 @@ class Sharing */ public static function get_token() { - // WebDAV has no concept of a query string and clients (including cadaver) - // seem to pass '?' unencoded, so we need to extract the path info out - // of the request URI ourselves - // if request URI contains a full url, remove schema and domain + // WebDAV has no concept of a query string and clients (including cadaver) + // seem to pass '?' unencoded, so we need to extract the path info out + // of the request URI ourselves + // if request URI contains a full url, remove schema and domain $matches = null; - if (preg_match('|^https?://[^/]+(/.*)$|', $path_info=$_SERVER['REQUEST_URI'], $matches)) - { - $path_info = $matches[1]; - } - $path_info = substr($path_info, strlen($_SERVER['SCRIPT_NAME'])); + if (preg_match('|^https?://[^/]+(/.*)$|', $path_info=$_SERVER['REQUEST_URI'], $matches)) + { + $path_info = $matches[1]; + } + $path_info = substr($path_info, strlen($_SERVER['SCRIPT_NAME'])); list(, $token/*, $path*/) = preg_split('|[/?]|', $path_info, 3); list($token) = explode(':', $token); @@ -140,16 +140,20 @@ class Sharing /** * Create sharing session * - * Certain cases: - * a) there is not session $keep_session === null - * --> create new anon session with just filemanager rights and share as fstab - * b) there is a session $keep_session === true - * b1) current user is share owner (eg. checking the link) - * --> mount share under token additionally - * b2) current user not share owner - * b2a) need/use filemanager UI (eg. directory) - * --> destroy current session and continue with a) - * b2b) single file or WebDAV + * There are two cases: + * + * 1) there is no session $keep_session === null + * --> create new anon session with just filemanager rights and resolved share incl. sharee as only fstab entry + * + * 2) there is a (non-anonymous) session $keep_session === true + * --> mount share with sharing stream-wrapper into users "shares" subdirectory of home directory + * and ask user if he wants the share permanently mounted there + * + * Even with sharing stream-wrapper a) and b) need to be different, as sharing SW needs an intact fstab! + * + * Not yet sure if this still needs extra handling: + * + * 2a) single file or WebDAV * --> modify EGroupware enviroment for that request only, no change in session * * @param boolean $keep_session =null null: create a new session, true: try mounting it into existing (already verified) session @@ -168,11 +172,22 @@ class Sharing return ''; } - protected static function check_token($keep_session, &$share) + /** + * Check sharing token + * + * @param boolean $keep_session false: does NOT check/fidle with session, true: return if session belongs to token + * @param array& $share on return information about the share + * @param ?string $token default call self::get_token() to get it from the URL + * @param ?string $password default $_SERVER['PHP_AUTH_PW'] + * @throws Exception + * @throws Exception\NoPermission + * @throws Exception\NotFound + */ + public static function check_token($keep_session, &$share, $token=null, $password=null) { self::$db = $GLOBALS['egw']->db; - $token = static::get_token(); + if (!isset($token)) $token = static::get_token(); // are we called from header include, because session did not verify // --> check if it verifys for our token @@ -203,7 +218,7 @@ class Sharing ); } // check password, if required - if(!static::check_password($share)) + if(!static::check_password($share, $password)) { $realm = 'EGroupware share '.$share['share_token']; header('WWW-Authenticate: Basic realm="'.$realm.'"'); @@ -220,14 +235,17 @@ class Sharing * provided matches. * * @param Array $share + * @param ?string $password default $_SERVER['PHP_AUTH_PW'] * @return boolean Password OK (or not needed) */ - protected static function check_password(Array $share) + protected static function check_password(Array $share, $password=null) { - if ($share['share_passwd'] && (empty($_SERVER['PHP_AUTH_PW']) || - !(Auth::compare_password($_SERVER['PHP_AUTH_PW'], $share['share_passwd'], 'crypt') || - Header\Authenticate::decode_password($_SERVER['PHP_AUTH_PW']) && - Auth::compare_password($_SERVER['PHP_AUTH_PW'], $share['share_passwd'], 'crypt')))) + if (!isset($password)) $password = $_SERVER['PHP_AUTH_PW']; + + if ($share['share_passwd'] && (empty($password) || + !(Auth::compare_password($password, $share['share_passwd'], 'crypt') || + Header\Authenticate::decode_password($password) && + Auth::compare_password($password, $share['share_passwd'], 'crypt')))) { return false; } @@ -246,7 +264,7 @@ class Sharing * Sub-class specific things needed to be done to the share (or session) * after we login but before we start actually doing anything */ - protected function after_login() {} + protected static function after_login(array $share) {} protected static function login($keep_session, &$share) @@ -267,7 +285,7 @@ class Sharing { $sessionid = static::create_new_session(); - $GLOBALS['egw']->sharing->after_login(); + static::after_login($share); } // we have a session we want to keep, but share owner is different from current user and we dont need filemanager UI // --> we dont need session and close it, to not modifiy it @@ -275,8 +293,7 @@ class Sharing { $GLOBALS['egw']->session->commit_session(); } - // need to store new fstab and vfs_user in session to allow GET requests / downloads via WebDAV - $GLOBALS['egw_info']['user']['vfs_user'] = Vfs::$user; + // need to store new fstab in session to allow GET requests / downloads via WebDAV $GLOBALS['egw_info']['server']['vfs_fstab'] = Vfs::mount(); // update modified egw and egw_info again in session, if neccessary @@ -369,7 +386,7 @@ class Sharing $class = strpos($status, '404') === 0 ? 'EGroupware\Api\Exception\NotFound' : strpos($status, '401') === 0 ? 'EGroupware\Api\Exception\NoPermission' : 'EGroupware\Api\Exception'; - throw new $class($message); + throw new $class($message, $status); } /** @@ -490,7 +507,7 @@ class Sharing public function ServeRequest() { // sharing is for a different share, change to current share - if ($this->share['share_token'] !== self::get_token()) + if (empty($this->share['skip_validate_token']) && $this->share['share_token'] !== self::get_token()) { // to keep the session we require the regular user flag "N" AND a user-name not equal to "anonymous" self::create_session($GLOBALS['egw']->session->session_flags === 'N' && @@ -499,14 +516,14 @@ class Sharing return $GLOBALS['egw']->sharing->ServeRequest(); } - // No extended ACL for readonly shares, disable eacl by setting session cache + /* No extended ACL for readonly shares, disable eacl by setting session cache if(!($this->share['share_writable'] & 1)) { Cache::setSession(Vfs\Sqlfs\StreamWrapper::EACL_APPNAME, 'extended_acl', array( '/' => 1, $this->share['share_path'] => 1 )); - } + }*/ if($this->use_collabora()) { $ui = new \EGroupware\Collabora\Ui(); diff --git a/api/src/Vfs.php b/api/src/Vfs.php index 1331866e1b..d6d7d24382 100644 --- a/api/src/Vfs.php +++ b/api/src/Vfs.php @@ -7,7 +7,7 @@ * @package api * @subpackage vfs * @author Ralf Becker - * @copyright (c) 2008-19 by Ralf Becker + * @copyright (c) 2008-20 by Ralf Becker */ namespace EGroupware\Api; @@ -66,45 +66,15 @@ use HTTP_WebDAV_Server; * 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 +class Vfs extends Vfs\Base { - const PREFIX = 'vfs://default'; - /** - * Scheme / protocol used for this stream-wrapper - */ - const SCHEME = Vfs\StreamWrapper::SCHEME; - /** - * Mime type of directories, the old vfs used 'Directory', while eg. WebDAV uses 'httpd/unix-directory' - */ - const DIR_MIME_TYPE = Vfs\StreamWrapper::DIR_MIME_TYPE; - /** - * 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; - /** - * mode-bits, which have to be set for links - */ - const MODE_LINK = Vfs\StreamWrapper::MODE_LINK; + const PREFIX = Vfs\StreamWrapper::PREFIX; + /** * Name of the lock table */ const LOCK_TABLE = 'egw_locks'; - /** - * How much should be logged to the apache error-log - * - * 0 = Nothing - * 1 = only errors - * 2 = all function calls and errors (contains passwords too!) - */ - const LOG_LEVEL = 1; + /** * Current user has root rights, no access checks performed! * @@ -325,45 +295,6 @@ class Vfs 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) - { - return Vfs\StreamWrapper::mount($url, $path, $check_url, $persitent_mount, $clear_fstab); - } - - /** - * Unmounts a filesystem part of the vfs - * - * @param string $path url or path of the filesystem to unmount - */ - static function umount($path) - { - return Vfs\StreamWrapper::umount($path); - } - - /** - * Returns mount url of a full url returned by resolve_url - * - * @param string $fullurl full url returned by resolve_url - * @return string|NULL mount url or null if not found - */ - static function mount_url($fullurl) - { - return Vfs\StreamWrapper::mount_url($fullurl); - } - /** * Check if file is hidden: name starts with a '.' or is Thumbs.db or _gsdata_ * @@ -384,6 +315,7 @@ class Vfs * * @param string|array $base base of the search * @param array $options =null the following keys are allowed: + * * - type => {d|f|F|!l} d=dirs, f=files (incl. symlinks), F=files (incl. symlinks to files), !l=no symlinks, 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) @@ -403,7 +335,8 @@ class Vfs * - follow => {true|false(default)} follow symlinks * - hidden => {true|false(default)} include hidden files (name starts with a '.' or is Thumbs.db) * - show-deleted => {true|false(default)} get also set by hidden, if not explicitly set otherwise (requires versioning!) - * @param string|array/true $exec =null function to call with each found file/dir as first param and stat array as last param or + * + * @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 @@ -421,11 +354,11 @@ class Vfs // 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'; + $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'; + $options['path_preg'] = '/^'.str_replace(array('\\?','\\*'),array('.{1}','.*'),preg_quote($options['path'], '/')).'$/i'; } if (!isset($options['uid'])) { @@ -460,7 +393,11 @@ class Vfs } // make all find options available as stream context option "find", to allow plugins to use them - $context = stream_context_create(array(self::SCHEME => array('find' => $options))); + $context = stream_context_create([ + self::SCHEME => [ + 'find' => $options, + ], + ]); $url = $options['url']; @@ -789,7 +726,7 @@ class Vfs * 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 string $path or url * @param int $check mode to check: one or more or'ed together of: 4 = self::READABLE, * 2 = self::WRITABLE, 1 = self::EXECUTABLE * @return boolean @@ -803,12 +740,13 @@ class Vfs * 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 string $path path or url * @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 + * @todo deprecated or even remove $user parameter and code */ static function check_access($path, $check, $stat=null, $user=null) { @@ -855,82 +793,15 @@ class Vfs 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 Exception\WrongParameter('path has to be string, use check_access($path,$check,$stat=null)!'); - } - // query stat array, if not given - if (is_null($stat)) - { - if (!isset($vfs)) $vfs = new Vfs\StreamWrapper(); - $stat = $vfs->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; - } - } - // if we check writable and have a readonly mount --> return false, as backends dont know about r/o url parameter - if ($check == self::WRITABLE && Vfs\StreamWrapper::url_is_readonly($stat['url'])) - { - //error_log(__METHOD__."(path=$path, check=writable, ...) failed because mount is readonly"); - return false; - } - // 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; + if (!isset($vfs)) $vfs = new Vfs\StreamWrapper($path); + return $vfs->check_access($path, $check, $stat); } /** * 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 string $path or url * @return boolean */ static function is_writable($path) @@ -942,7 +813,7 @@ class Vfs * 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 string $path or url * @return boolean */ static function is_executable($path) @@ -953,7 +824,7 @@ class Vfs /** * Check if path is a script and write access would be denied by backend * - * @param string $path + * @param string $path or url * @return boolean true if $path is a script AND exec mount-option is NOT set, false otherwise */ static function deny_script($path) @@ -1038,7 +909,7 @@ class Vfs */ static function proppatch($path,array $props) { - return self::_call_on_backend('proppatch',array($path,$props)); + return self::_call_on_backend('proppatch', [$path,$props], false, 0, true); } /** @@ -1057,7 +928,7 @@ class Vfs */ 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) + return self::_call_on_backend('propfind', [$path, $ns],true, 0, true); // true = fail silent (no PHP Warning) } /** @@ -1380,21 +1251,12 @@ class Vfs * 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 + * @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) - { - $vfs = new Vfs\StreamWrapper(); - $stat = $vfs->url_stat($path,0); - } - return $stat['uid'] == self::$user && // (current) user is the owner - // in sharing current user != self::$user and should NOT have owner rights - $GLOBALS['egw_info']['user']['account_id'] == self::$user || - self::$is_root || // class runs with root rights - !$stat['uid'] && $stat['gid'] && self::$is_admin; // group directory and user is an eGW admin + return (new Vfs\StreamWrapper())->has_owner_rights($path, $stat); } /** @@ -1665,24 +1527,25 @@ class Vfs /** * lock a ressource/path * - * @param string $path path or url + * @param string $url url or path, lock is granted for the path only, but url is used for access checks * @param string &$token * @param int &$timeout - * @param string &$owner + * @param int|string &$owner account_id, account_lid or mailto-url * @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) + static function lock($url, &$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) && ($dir=self::dirname($path)) && self::is_writable($dir))) + if (!$url || $update && !$token || $check_writable && + !(self::is_writable($url) || !self::file_exists($url) && ($dir=self::dirname($url)) && self::is_writable($dir))) { return false; } + $path = self::parse_url($url, PHP_URL_PATH); // remove the lock info evtl. set in the cache unset(self::$lock_cache[$path]); @@ -1712,17 +1575,21 @@ class Vfs } } // 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')) + elseif(($lock = self::checkLock($url)) && ($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') + if (!$owner || $owner === 'unknown') { $owner = 'mailto:'.$GLOBALS['egw_info']['user']['account_email']; } + elseif (($email = Accounts::id2name($owner, 'account_email'))) + { + $owner = 'mailto:'.$email; + } if (!$token) { require_once(__DIR__.'/WebDAV/Server.php'); @@ -1746,48 +1613,50 @@ class Vfs $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')); + if (self::LOCK_DEBUG) error_log(__METHOD__."($url,$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 $url url or path, lock is granted for the path only, but url is used for access checks * @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) + static function unlock($url,$token,$check_writable=true) { // we require write rights to lock/unlock a resource - if ($check_writable && !self::is_writable($path)) + if ($check_writable && !self::is_writable($url)) { 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')); + $path = self::parse_url($url, PHP_URL_PATH); + 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__."($url,$token,$check_writable) returns ".($ret ? 'true' : 'false')); return $ret; } /** * checkLock() helper * - * @param string resource path to check for locks + * @param string $url url or path, lock is granted for the path only, but url is used for access checks * @return array|boolean false if there's no lock, else array with lock info */ - static function checkLock($path) + static function checkLock($url) { + $path = self::parse_url($url, PHP_URL_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))); + if (self::LOCK_DEBUG) error_log(__METHOD__."($url) returns from CACHE ".str_replace(array("\n",' '),'',print_r(self::$lock_cache[$url],true))); return self::$lock_cache[$path]; } $where = 'lock_path='.self::$db->quote($path); @@ -1808,10 +1677,10 @@ class Vfs '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"); + if (self::LOCK_DEBUG) error_log(__METHOD__."($url) 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')); + if (self::LOCK_DEBUG) error_log(__METHOD__."($url) returns ".($result?array2string($result):'false')); return self::$lock_cache[$path] = $result; } @@ -1914,9 +1783,7 @@ class Vfs */ 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::$user = (int)$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(); @@ -2212,25 +2079,10 @@ class Vfs */ static function resolve_url_symlinks($_path,$file_exists=true,$resolve_last_symlink=true,&$stat=null) { - $vfs = new Vfs\StreamWrapper(); + $vfs = new Vfs\StreamWrapper($_path); return $vfs->resolve_url_symlinks($_path, $file_exists, $resolve_last_symlink, $stat); } - /** - * Resolve the given path according to our fstab - * - * @param string $_path - * @param boolean $do_symlink =true is a direct match allowed, default yes (must be false for a lstat or readlink!) - * @param boolean $use_symlinkcache =true - * @param boolean $replace_user_pass_host =true replace $user,$pass,$host in url, default true, if false result is not cached - * @param boolean $fix_url_query =false true append relativ path to url query parameter, default not - * @return string|boolean false if the url cant be resolved, should not happen if fstab has a root entry - */ - static function resolve_url($_path,$do_symlink=true,$use_symlinkcache=true,$replace_user_pass_host=true,$fix_url_query=false) - { - return Vfs\StreamWrapper::resolve_url($_path, $do_symlink, $use_symlinkcache, $replace_user_pass_host, $fix_url_query); - } - /** * This method is called in response to mkdir() calls on URL paths associated with the wrapper. * @@ -2276,77 +2128,6 @@ class Vfs return $path[0] == '/' && unlink(self::PREFIX.$path); } - /** - * Allow to call methods of the underlying stream wrapper: touch, chmod, chgrp, chown, ... - * - * We cant use a magic __call() method, as it does not work for static methods! - * - * @param string $name - * @param array $params first param has to be the path, otherwise we can not determine the correct wrapper - * @param boolean $fail_silent =false should only false be returned if function is not supported by the backend, - * or should an E_USER_WARNING error be triggered (default) - * @param int $path_param_key =0 key in params containing the path, default 0 - * @return mixed return value of backend or false if function does not exist on backend - */ - static protected function _call_on_backend($name,$params,$fail_silent=false,$path_param_key=0) - { - $pathes = $params[$path_param_key]; - - $scheme2urls = array(); - foreach(is_array($pathes) ? $pathes : array($pathes) as $path) - { - if (!($url = self::resolve_url_symlinks($path,false,false))) - { - return false; - } - $k=(string)self::parse_url($url,PHP_URL_SCHEME); - if (!(is_array($scheme2urls[$k]))) $scheme2urls[$k] = array(); - $scheme2urls[$k][$path] = $url; - } - $ret = array(); - foreach($scheme2urls as $scheme => $urls) - { - if ($scheme) - { - if (!class_exists($class = self::scheme2class($scheme)) || !method_exists($class,$name)) - { - if (!$fail_silent) trigger_error("Can't $name for scheme $scheme!\n",E_USER_WARNING); - return false; - } - if (!is_array($pathes)) - { - $params[$path_param_key] = $url; - - return call_user_func_array(array($class,$name),$params); - } - $params[$path_param_key] = $urls; - if (!is_array($r = call_user_func_array(array($class,$name),$params))) - { - return $r; - } - // we need to re-translate the urls to pathes, as they can eg. contain symlinks - foreach($urls as $path => $url) - { - if (isset($r[$url]) || isset($r[$url=self::parse_url($url,PHP_URL_PATH)])) - { - $ret[$path] = $r[$url]; - } - } - } - // call the filesystem specific function (dont allow to use arrays!) - elseif(!function_exists($name) || is_array($pathes)) - { - return false; - } - else - { - $time = null; - return $name($url,$time); - } - } - return $ret; - } - /** * touch just running on VFS path * @@ -2412,7 +2193,7 @@ class Vfs */ static function readlink($path) { - $ret = self::_call_on_backend('readlink',array($path),true); // true = fail silent, if backend does not support readlink + $ret = self::_call_on_backend('readlink', [$path],true, 0, true); // true = fail silent, if backend does not support readlink //error_log(__METHOD__."('$path') returning ".array2string($ret).' '.function_backtrace()); return $ret; } @@ -2428,7 +2209,7 @@ class Vfs */ static function symlink($target,$link) { - if (($ret = self::_call_on_backend('symlink',array($target,$link),false,1))) // 1=path is in $link! + if (($ret = self::_call_on_backend('symlink', [$target, $link],false,1, true))) // 1=path is in $link! { Vfs\StreamWrapper::symlinkCache_remove($link); } @@ -2515,9 +2296,9 @@ class Vfs static function clearstatcache($path='/') { //error_log(__METHOD__."('$path')"); - Vfs\StreamWrapper::clearstatcache($path); + parent::clearstatcache($path); self::_call_on_backend('clearstatcache', array($path), true, 0); - Vfs\StreamWrapper::clearstatcache($path); + parent::clearstatcache($path); } /** diff --git a/api/src/Vfs/Base.php b/api/src/Vfs/Base.php new file mode 100644 index 0000000000..f451d07326 --- /dev/null +++ b/api/src/Vfs/Base.php @@ -0,0 +1,578 @@ + + * @copyright (c) 2008-20 by Ralf Becker + */ + +namespace EGroupware\Api\Vfs; + +use EGroupware\Api\Config; +use EGroupware\Api\Vfs; + +/** + * Shared base of Vfs class and Vfs-stream-wrapper + */ +class Base +{ + /** + * Scheme / protocol used for this stream-wrapper + */ + const SCHEME = 'vfs'; + /** + * Mime type of directories, the old vfs used 'Directory', while eg. WebDAV uses 'httpd/unix-directory' + */ + const DIR_MIME_TYPE = 'httpd/unix-directory'; + /** + * 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; + /** + * 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!) + */ + const LOG_LEVEL = 1; + + /** + * Our fstab in the form mount-point => url + * + * The entry for root has to be the first, or more general if you mount into subdirs the parent has to be before! + * + * @var array + */ + protected static $fstab = array( + '/' => 'sqlfs://$host/', + '/apps' => 'links://$host/apps', + ); + + /** + * Mounts $url under $path in the vfs, called without parameter it returns the fstab + * + * The fstab is stored in the eGW configuration and used for all eGW users. + * + * @param string $url =null url of the filesystem to mount, eg. oldvfs://default/ + * @param string $path =null path to mount the filesystem in the vfs, eg. / + * @param boolean $check_url =null check if url is an existing directory, before mounting it + * default null only checks if url does not contain a $ as used in $user or $pass + * @param boolean $persitent_mount =true create a persitent mount, or only a temprary for current request + * @param boolean $clear_fstab =false true clear current fstab, false (default) only add given mount + * @return array|boolean array with fstab, if called without parameter or true on successful mount + */ + static function mount($url=null,$path=null,$check_url=null,$persitent_mount=true,$clear_fstab=false) + { + if (is_null($check_url)) $check_url = strpos($url,'$') === false; + + if (!isset($GLOBALS['egw_info']['server']['vfs_fstab'])) // happens eg. in setup + { + $api_config = Config::read('phpgwapi'); + if (isset($api_config['vfs_fstab']) && is_array($api_config['vfs_fstab'])) + { + self::$fstab = $api_config['vfs_fstab']; + } + else + { + self::$fstab = array( + '/' => 'sqlfs://$host/', + '/apps' => 'links://$host/apps', + ); + } + unset($api_config); + } + if (is_null($url) || is_null($path)) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__.'('.array2string($url).','.array2string($path).') returns '.array2string(self::$fstab)); + return self::$fstab; + } + if (!Vfs::$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(Vfs::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, 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 (!Vfs::$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; + } + + /** + * Returns mount url of a full url returned by resolve_url + * + * @param string $fullurl full url returned by resolve_url + * @return string|NULL mount url or null if not found + */ + static function mount_url($fullurl, &$mounted=null) + { + foreach(array_reverse(self::$fstab) as $mounted => $url) + { + list($url_no_query) = explode('?',$url); + if (substr($fullurl,0,1+strlen($url_no_query)) === $url_no_query.'/') + { + return $url; + } + } + return null; + } + + /** + * Cache of already resolved urls + * + * @var array with path => target + */ + private static $resolve_url_cache = array(); + + private static $wrappers; + + /** + * Resolve the given path according to our fstab + * + * @param string $_path + * @param boolean $do_symlink =true is a direct match allowed, default yes (must be false for a lstat or readlink!) + * @param boolean $use_symlinkcache =true + * @param boolean $replace_user_pass_host =true replace $user,$pass,$host in url, default true, if false result is not cached + * @param boolean $fix_url_query =false true append relativ path to url query parameter, default not + * @param ?string &$mounted =null on return mount-point of resolved url, IF $_path is a path or vfs-url, other urls return NULL! + * @return string|boolean false if the url cant be resolved, should not happen if fstab has a root entry + */ + static function resolve_url($_path,$do_symlink=true,$use_symlinkcache=true,$replace_user_pass_host=true,$fix_url_query=false, &$mounted=null) + { + $path = self::get_path($_path); + + // we do some caching here + if (isset(self::$resolve_url_cache[$path]) && $replace_user_pass_host) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__."('$path') = '".self::$resolve_url_cache[$path]."' (from cache)"); + $mounted = self::$resolve_url_cache[$path]['mounted']; + return self::$resolve_url_cache[$path]['url']; + } + // check if we can already resolve path (or a part of it) with a known symlinks + if ($use_symlinkcache) + { + $path = self::symlinkCache_resolve($path,$do_symlink); + } + // setting default user, passwd and domain, if it's not contained int the url + $defaults = array( + 'user' => $GLOBALS['egw_info']['user']['account_lid'], + 'pass' => urlencode($GLOBALS['egw_info']['user']['passwd']), + 'host' => $GLOBALS['egw_info']['user']['domain'], + 'home' => str_replace(array('\\\\','\\'),array('','/'),$GLOBALS['egw_info']['user']['homedirectory']), + ); + $parts = array_merge(Vfs::parse_url($path),$defaults); + if (!$parts['host']) $parts['host'] = 'default'; // otherwise we get an invalid url (scheme:///path/to/something)! + + if (!empty($parts['scheme']) && $parts['scheme'] != self::SCHEME) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__."('$path') = '$path' (path is already an url)"); + return $path; // path is already a non-vfs url --> nothing to do + } + if (empty($parts['path'])) $parts['path'] = '/'; + + foreach(array_reverse(self::$fstab) as $mounted => $url) + { + if ($mounted == '/' || $mounted == $parts['path'] || $mounted.'/' == substr($parts['path'],0,strlen($mounted)+1)) + { + $scheme = Vfs::parse_url($url,PHP_URL_SCHEME); + if (is_null(self::$wrappers) || !in_array($scheme,self::$wrappers)) + { + self::load_wrapper($scheme); + } + if (($relative = substr($parts['path'],strlen($mounted)))) + { + $url = Vfs::concat($url,$relative); + } + // if url contains url parameter, eg. from filesystem streamwrapper, we need to append relative path here too + $matches = null; + if ($fix_url_query && preg_match('|([?&]url=)([^&]+)|', $url, $matches)) + { + $url = str_replace($matches[0], $matches[1].Vfs::concat($matches[2], substr($parts['path'],strlen($mounted))), $url); + } + + if ($replace_user_pass_host) + { + $url = str_replace(array('$user','$pass','$host','$home'),array($parts['user'],$parts['pass'],$parts['host'],$parts['home']),$url); + } + if ($parts['query']) $url .= '?'.$parts['query']; + if ($parts['fragment']) $url .= '#'.$parts['fragment']; + + if (self::LOG_LEVEL > 1) error_log(__METHOD__."('$path') = '$url'"); + + if (($class = self::scheme2class($scheme)) && is_callable([$class, 'replace'])) + { + if (!($replace = call_user_func([$class, 'replace'], $url))) + { + return false; + } + $url = $replace; + } + if ($replace_user_pass_host) self::$resolve_url_cache[$path] = ['url' => $url, 'mounted' => $mounted]; + + return $url; + } + } + if (self::LOG_LEVEL > 0) error_log(__METHOD__."('$path') can't resolve path!\n"); + trigger_error(__METHOD__."($path) can't resolve path!\n",E_USER_WARNING); + return false; + } + + /** + * Cache of already resolved symlinks + * + * @var array with path => target + */ + private static $symlink_cache = array(); + + /** + * Add a resolved symlink to cache + * + * @param string $_path vfs path + * @param string $target target path + */ + static protected function symlinkCache_add($_path,$target) + { + $path = self::get_path($_path); + + if (isset(self::$symlink_cache[$path])) return; // nothing to do + + if ($target[0] != '/') $target = Vfs::parse_url($target,PHP_URL_PATH); + + self::$symlink_cache[$path] = $target; + + // sort longest path first + uksort(self::$symlink_cache, function($b, $a) + { + return strlen($a) - strlen($b); + }); + if (self::LOG_LEVEL > 1) error_log(__METHOD__."($path,$target) cache now ".array2string(self::$symlink_cache)); + } + + /** + * Remove a resolved symlink from cache + * + * @param string $_path vfs path + */ + static public function symlinkCache_remove($_path) + { + $path = self::get_path($_path); + + unset(self::$symlink_cache[$path]); + if (self::LOG_LEVEL > 1) error_log(__METHOD__."($path) cache now ".array2string(self::$symlink_cache)); + } + + /** + * Resolve a path from our symlink cache + * + * The cache is sorted from longer to shorter pathes. + * + * @param string $_path + * @param boolean $do_symlink =true is a direct match allowed, default yes (must be false for a lstat or readlink!) + * @return string target or path, if path not found + */ + public static function symlinkCache_resolve($_path, $do_symlink=true) + { + // remove vfs scheme, but no other schemes (eg. filesystem!) + $path = self::get_path($_path); + + $strlen_path = strlen($path); + + foreach(self::$symlink_cache as $p => $t) + { + if (($strlen_p = strlen($p)) > $strlen_path) continue; // $path can NOT start with $p + + if ($path == $p) + { + if ($do_symlink) $target = $t; + break; + } + elseif (substr($path,0,$strlen_p+1) == $p.'/') + { + $target = $t . substr($path,$strlen_p); + break; + } + } + if (self::LOG_LEVEL > 1 && isset($target)) error_log(__METHOD__."($path) = $target"); + return isset($target) ? $target : $path; + } + + /** + * Clears our internal stat and symlink cache + * + * Normaly not necessary, as it is automatically cleared/updated, UNLESS Vfs::$user changes! + */ + static function clearstatcache() + { + self::$symlink_cache = self::$resolve_url_cache = array(); + } + + /** + * Load stream wrapper for a given schema + * + * @param string $scheme + * @return boolean + */ + static function load_wrapper($scheme) + { + if (!in_array($scheme,self::get_wrappers())) + { + switch($scheme) + { + case 'webdav': + case 'webdavs': + \Grale\WebDav\StreamWrapper::register(); + self::$wrappers[] = 'webdav'; + self::$wrappers[] = 'webdavs'; + break; + case '': + break; // default file, always loaded + default: + // check if scheme is buildin in php or one of our own stream wrappers + if (in_array($scheme,stream_get_wrappers()) || class_exists(self::scheme2class($scheme))) + { + self::$wrappers[] = $scheme; + } + else + { + trigger_error("Can't load stream-wrapper for scheme '$scheme'!",E_USER_WARNING); + return false; + } + } + } + return true; + } + + /** + * Return already loaded stream wrappers + * + * @return array + */ + static function get_wrappers() + { + if (is_null(self::$wrappers)) + { + self::$wrappers = stream_get_wrappers(); + } + return self::$wrappers; + } + + /** + * Get the class-name for a scheme + * + * A scheme is not allowed to contain an underscore, but allows a dot and a class names only allow or need underscores, but no dots + * --> we replace dots in scheme with underscored to get the class-name + * + * @param string $scheme eg. vfs + * @return string + */ + static function scheme2class($scheme) + { + if ($scheme === self::SCHEME) + { + return __CLASS__; + } + 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; + } + } + + /** + * Getting the path from an url (or path) AND removing trailing slashes + * + * @param string $path url or path (might contain trailing slash from WebDAV!) + * @param string $only_remove_scheme =self::SCHEME if given only that scheme get's removed + * @return string path without training slash + */ + static protected function get_path($path,$only_remove_scheme=self::SCHEME) + { + if ($path[0] != '/' && (!$only_remove_scheme || Vfs::parse_url($path, PHP_URL_SCHEME) == $only_remove_scheme)) + { + $path = Vfs::parse_url($path, PHP_URL_PATH); + } + // remove trailing slashes eg. added by WebDAV, but do NOT remove / from "sqlfs://default/"! + if ($path != '/') + { + while (mb_substr($path, -1) == '/' && $path != '/' && ($path[0] == '/' || Vfs::parse_url($path, PHP_URL_PATH) != '/')) + { + $path = mb_substr($path,0,-1); + } + } + return $path; + } + + /** + * Check if url contains ro=1 parameter to mark mount readonly + * + * @param string $url + * @return boolean + */ + static function url_is_readonly($url) + { + static $cache = array(); + $ret =& $cache[$url]; + if (!isset($ret)) + { + $matches = null; + $ret = preg_match('/\?(.*&)?ro=([^&]+)/', $url, $matches) && $matches[2]; + } + return $ret; + } + + /** + * Allow to call methods of the underlying stream wrapper: touch, chmod, chgrp, chown, ... + * + * We cant use a magic __call() method, as it does not work for static methods! + * + * @param string $name + * @param array $params first param has to be the path, otherwise we can not determine the correct wrapper + * @param boolean|"null" $fail_silent =false should only false be returned if function is not supported by the backend, + * or should an E_USER_WARNING error be triggered (default), or "null": return NULL + * @param int $path_param_key =0 key in params containing the path, default 0 + * @param boolean $instanciate =false true: instanciate the class to call method $name, false: static call + * @return mixed return value of backend or false if function does not exist on backend + */ + protected static function _call_on_backend($name, array $params, $fail_silent=false, $path_param_key=0, $instanciate=false) + { + $pathes = $params[$path_param_key]; + + $scheme2urls = array(); + foreach(is_array($pathes) ? $pathes : array($pathes) as $path) + { + if (!($url = Vfs::resolve_url_symlinks($path,false,false))) + { + return false; + } + $k=(string)Vfs::parse_url($url,PHP_URL_SCHEME); + if (!(is_array($scheme2urls[$k]))) $scheme2urls[$k] = array(); + $scheme2urls[$k][$path] = $url; + } + $ret = array(); + foreach($scheme2urls as $scheme => $urls) + { + if ($scheme) + { + if (!class_exists($class = Vfs\StreamWrapper::scheme2class($scheme)) || !method_exists($class,$name)) + { + if (!$fail_silent) trigger_error("Can't $name for scheme $scheme!\n",E_USER_WARNING); + return $fail_silent === 'null' ? null : false; + } + $callback = [$instanciate ? new $class($url) : $class, $name]; + if (!is_array($pathes)) + { + $params[$path_param_key] = $url; + + return call_user_func_array($callback, $params); + } + $params[$path_param_key] = $urls; + if (!is_array($r = call_user_func_array($callback, $params))) + { + return $r; + } + // we need to re-translate the urls to pathes, as they can eg. contain symlinks + foreach($urls as $path => $url) + { + if (isset($r[$url]) || isset($r[$url=Vfs::parse_url($url,PHP_URL_PATH)])) + { + $ret[$path] = $r[$url]; + } + } + } + // call the filesystem specific function (dont allow to use arrays!) + elseif(!function_exists($name) || is_array($pathes)) + { + return false; + } + else + { + $time = null; + return $name($url,$time); + } + } + return $ret; + } +} diff --git a/api/src/Vfs/Links/StreamWrapper.php b/api/src/Vfs/Links/StreamWrapper.php index b8e4e7d1dc..fd7a4ac5b1 100644 --- a/api/src/Vfs/Links/StreamWrapper.php +++ b/api/src/Vfs/Links/StreamWrapper.php @@ -1,14 +1,13 @@ - * @copyright (c) 2008-16 by Ralf Becker - * @version $Id: class.sqlfs_stream_wrapper.inc.php 24997 2008-03-02 21:44:15Z ralfbecker $ + * @copyright (c) 2008-20 by Ralf Becker */ namespace EGroupware\Api\Vfs\Links; @@ -76,7 +75,7 @@ class StreamWrapper extends LinksParent * @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) + function check_extended_acl($url,$check) { if (Vfs::$is_root) { @@ -96,7 +95,7 @@ class StreamWrapper extends LinksParent $access = !($check & Vfs::WRITABLE); // always grant read access to /apps $what = '!$app'; } - elseif (!self::check_app_rights($app)) + elseif (!$this->check_app_rights($app)) { $access = false; // user has no access to the $app application $what = 'no app-rights'; @@ -113,8 +112,8 @@ class StreamWrapper extends LinksParent { // vfs & stream-wrapper use posix rights, Api\Link::file_access uses Api\Acl::{EDIT|READ}! $required = $check & Vfs::WRITABLE ? Api\Acl::EDIT : Api\Acl::READ; - $access = Api\Link::file_access($app,$id,$required,$rel_path,Vfs::$user); - $what = "from Api\Link::file_access('$app',$id,$required,'$rel_path,".Vfs::$user.")"; + $access = Api\Link::file_access($app, $id, $required, $rel_path, $this->user); + $what = "from Api\Link::file_access('$app', $id, $required, '$rel_path,".$this->user.")"; } if (self::DEBUG) error_log(__METHOD__."($url,$check) user=".Vfs::$user." ($what) ".($access?"access granted ($app:$id:$rel_path)":'no access!!!')); return $access; @@ -126,18 +125,18 @@ class StreamWrapper extends LinksParent * @param string $app * @return boolean */ - public static function check_app_rights($app) + public function check_app_rights($app) { - if ($GLOBALS['egw_info']['user']['account_id'] == Vfs::$user) + if ($GLOBALS['egw_info']['user']['account_id'] == $this->user && isset($GLOBALS['egw_info']['user']['apps'])) { return isset($GLOBALS['egw_info']['user']['apps'][$app]); } static $user_apps = array(); - if (!isset($user_apps[Vfs::$user])) + if (!isset($user_apps[$this->user])) { - $user_apps[Vfs::$user] = $GLOBALS['egw']->acl->get_user_applications(Vfs::$user); + $user_apps[$this->user] = $GLOBALS['egw']->acl->get_user_applications($this->user); } - return !empty($user_apps[Vfs::$user][$app]); + return !empty($user_apps[$this->user][$app]); } /** @@ -159,48 +158,52 @@ class StreamWrapper extends LinksParent */ function url_stat ( $url, $flags ) { - $eacl_check=self::check_extended_acl($url,Vfs::READABLE); + $this->check_set_context($url); - // return vCard as /.entry - if ( $eacl_check && substr($url,-7) == '/.entry' && - (list($app) = array_slice(explode('/',$url),-3,1)) && $app === 'addressbook') + $ret = false; + if (($eacl_check = $this->check_extended_acl($url,Vfs::READABLE))) { - $ret = array( - 'ino' => '#'.md5($url), - 'name' => '.entry', - '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, - 'mtime' => time(), - 'ctime' => time(), - 'nlink' => 1, - // eGW addition to return some extra values - 'mime' => $app == 'addressbook' ? 'text/vcard' : 'text/calendar', - ); - } - // if entry directory does not exist --> return fake directory - elseif (!($ret = parent::url_stat($url,$flags)) && $eacl_check) - { - list(,/*$apps*/,/*$app*/,$id,$rel_path) = array_pad(explode('/', Vfs::parse_url($url, PHP_URL_PATH), 5),5,null); - if ($id && !isset($rel_path)) + // return vCard as /.entry + if (substr($url, -7) == '/.entry' && + (list($app) = array_slice(explode('/', $url), -3, 1)) && $app === 'addressbook') { $ret = array( - 'ino' => '#'.md5($url), - 'name' => $id, - 'mode' => self::MODE_DIR, // required by the stream wrapper - 'size' => 0, - 'uid' => 0, - 'gid' => 0, + 'ino' => '#' . md5($url), + 'name' => '.entry', + 'mode' => self::MODE_FILE | Vfs::READABLE, // required by the stream wrapper + 'size' => 1024, // email does NOT attach files with size 0! + 'uid' => 0, + 'gid' => 0, 'mtime' => time(), 'ctime' => time(), - 'nlink' => 2, + 'nlink' => 1, // eGW addition to return some extra values - 'mime' => Vfs::DIR_MIME_TYPE, + 'mime' => $app == 'addressbook' ? 'text/vcard' : 'text/calendar', ); } + // if entry directory does not exist --> return fake directory + elseif (!($ret = parent::url_stat($url, $flags))) + { + list(,/*$apps*/,/*$app*/, $id, $rel_path) = array_pad(explode('/', Vfs::parse_url($url, PHP_URL_PATH), 5), 5, null); + if ($id && !isset($rel_path)) + { + $ret = array( + 'ino' => '#' . md5($url), + 'name' => $id, + 'mode' => self::MODE_DIR, // required by the stream wrapper + 'size' => 0, + 'uid' => 0, + 'gid' => 0, + 'mtime' => time(), + 'ctime' => time(), + 'nlink' => 2, + // eGW addition to return some extra values + 'mime' => Vfs::DIR_MIME_TYPE, + ); + } + } } - if (self::DEBUG) error_log(__METHOD__."('$url', $flags) calling parent::url_stat(,,".array2string($eacl_check).') returning '.array2string($ret)); + if (self::DEBUG) error_log(__METHOD__."('$url', $flags) eacl_check=".array2string($eacl_check).' returning '.array2string($ret)); return $ret; } @@ -264,7 +267,7 @@ class StreamWrapper extends LinksParent list(,$apps,$app,$id) = explode('/',$path); $ret = false; - if ($apps == 'apps' && $app && !$id || self::check_extended_acl($path,Vfs::WRITABLE)) // app directory itself is allways ok + if ($apps == 'apps' && $app && !$id || $this->check_extended_acl($path,Vfs::WRITABLE)) // app directory itself is allways ok { $current_is_root = Vfs::$is_root; Vfs::$is_root = true; $current_user = Vfs::$user; Vfs::$user = 0; @@ -335,7 +338,7 @@ class StreamWrapper extends LinksParent { $charset = 'utf-8'; } - if (!($vcard =& $ab_vcard->getVCard($id, $charset))) + if (!($vcard = $ab_vcard->getVCard($id, $charset))) { error_log(__METHOD__."('$url', '$mode', $options) addressbook_vcal::getVCard($id) returned false!"); return false; @@ -348,7 +351,7 @@ class StreamWrapper extends LinksParent } // create not existing entry directories on the fly if ($mode[0] != 'r' && ($dir = Vfs::dirname($url)) && - !parent::url_stat($dir, 0) && self::check_extended_acl($dir, Vfs::WRITABLE)) + !parent::url_stat($dir, 0) && $this->check_extended_acl($dir, Vfs::WRITABLE)) { $this->mkdir($dir,0,STREAM_MKDIR_RECURSIVE); } @@ -431,14 +434,14 @@ class StreamWrapper extends LinksParent * @param string $link * @return boolean true on success false on error */ - static function symlink($target,$link) + function symlink($target,$link) { - $parent = new \EGroupware\Api\Vfs\Links\LinksParent(); - if (!$parent->url_stat($dir = Vfs::dirname($link),0) && self::check_extended_acl($dir,Vfs::WRITABLE)) + $parent = new \EGroupware\Api\Vfs\Links\LinksParent($target); + if (!$parent->url_stat($dir = Vfs::dirname($link),0) && $this->check_extended_acl($dir,Vfs::WRITABLE)) { $parent->mkdir($dir,0,STREAM_MKDIR_RECURSIVE); } - return parent::symlink($target,$link); + return $parent->symlink($target,$link); } /** @@ -446,7 +449,7 @@ class StreamWrapper extends LinksParent */ public static function register() { - stream_register_wrapper(self::SCHEME, __CLASS__); + stream_wrapper_register(self::SCHEME, __CLASS__); } } diff --git a/api/src/Vfs/Sharing.php b/api/src/Vfs/Sharing.php index adf12b5cbf..7d4511a960 100644 --- a/api/src/Vfs/Sharing.php +++ b/api/src/Vfs/Sharing.php @@ -15,6 +15,7 @@ namespace EGroupware\Api\Vfs; use EGroupware\Api; use EGroupware\Api\Vfs; +use EGroupware\Collabora\Wopi; use filemanager_ui; /** @@ -71,19 +72,28 @@ class Sharing extends \EGroupware\Api\Sharing ), ); + /** + * Subdirectory of user's home directory to mount shares into + */ + const SHARES_DIRECTORY = 'shares'; + /** * Create sharing session * - * Certain cases: - * a) there is not session $keep_session === null - * --> create new anon session with just filemanager rights and share as fstab - * b) there is a session $keep_session === true - * b1) current user is share owner (eg. checking the link) - * --> mount share under token additionally - * b2) current user not share owner - * b2a) need/use filemanager UI (eg. directory) - * --> destroy current session and continue with a) - * b2b) single file or WebDAV + * There are two cases: + * + * 1) there is no session $keep_session === null + * --> create new anon session with just filemanager rights and resolved share incl. sharee as only fstab entry + * + * 2) there is a (non-anonymous) session $keep_session === true + * --> mount share with sharing stream-wrapper into users "shares" subdirectory of home directory + * and ask user if he wants the share permanently mounted there + * + * Even with sharing stream-wrapper a) and b) need to be different, as sharing SW needs an intact fstab! + * + * Not yet sure if this still needs extra handling: + * + * 2a) single file or WebDAV * --> modify EGroupware enviroment for that request only, no change in session * * @param boolean $keep_session =null null: create a new session, true: try mounting it into existing (already verified) session @@ -98,54 +108,64 @@ class Sharing extends \EGroupware\Api\Sharing $GLOBALS['egw_info']['server']['vfs_fstab'] = Vfs::mount(); Vfs::clearstatcache(); } - $share['resolve_url'] = Vfs::resolve_url($share['share_path'], true, true, true, true); // true = fix evtl. contained url parameter + + // for a regular user session, mount the share into "shares" subdirectory of his home-directory + if ($keep_session && $GLOBALS['egw_info']['user']['account_lid'] && $GLOBALS['egw_info']['user']['account_lid'] !== 'anonymous') + { + $shares_dir = '/home/'.Vfs::encodePathComponent($GLOBALS['egw_info']['user']['account_lid']).'/'.self::SHARES_DIRECTORY; + if (!Vfs::file_exists($shares_dir)) Vfs::mkdir($shares_dir, 0750, true); + $share['share_root'] = Vfs::concat($shares_dir, Vfs::basename($share['share_path'])); + + // ToDo: handle there's already something there with that name (incl. maybe the same share!) + + Vfs::$is_root = true; + if (!Vfs::mount(Vfs\Sharing\StreamWrapper::share2url($share), $share['share_root'], false, false, $clear_fstab)) + { + sleep(1); + return static::share_fail( + '404 Not Found', + "Requested resource '/".htmlspecialchars($share['share_token'])."' does NOT exist!\n" + ); + } + Vfs::$is_root = false; + + Api\Framework::message(lang('Share has been mounted into you shares directory').': '.$share['share_root'], 'success'); + // ToDo: ask user if he want's the share permanently mounted + return; + } + + /** + * From here on pure sharing url without regular EGroupware user (session) + */ + $share['resolve_url'] = Vfs::build_url([ + 'user' => Api\Accounts::id2name($share['share_owner']), + ]+Vfs::parse_url(Vfs::resolve_url($share['share_path'], true, true, true, true))); // true = fix evtl. contained url parameter // if share not writable append ro=1 to mount url to make it readonly if (!($share['share_writable'] & 1)) { $share['resolve_url'] .= (strpos($share['resolve_url'], '?') ? '&' : '?').'ro=1'; } - //_debug_array($share); + $share['share_root'] = '/'; - $share['share_root'] = '/'.Vfs::basename($share['share_path']); - if ($keep_session) // add share to existing session + // only allow filemanager app & collabora + // In some cases, $GLOBALS['egw_info']['apps'] is not yet set at all. Set it to app => true, it will be used + // in Session->read_repositories() to make sure we get access to these apps when the session loads the apps. + $apps = $GLOBALS['egw']->acl->get_user_applications($share['share_owner']); + $GLOBALS['egw_info']['user']['apps'] = array( + 'filemanager' => $GLOBALS['egw_info']['apps']['filemanager'] || true, + 'collabora' => $GLOBALS['egw_info']['apps']['collabora'] || $apps['collabora'] + ); + + // Need to re-init stream wrapper, as some of them look at preferences or permissions + $class = Vfs\StreamWrapper::scheme2class(Vfs::parse_url($share['resolve_url'],PHP_URL_SCHEME)); + if($class && method_exists($class, 'init_static')) { - // if current user is not the share owner, we cant just mount share - if (Vfs::$user != $share['share_owner']) - { - $keep_session = false; - } - } - if (!$keep_session) // do NOT change to else, as we might have set $keep_session=false! - { - // only allow filemanager app & collabora - // In some cases, $GLOBALS['egw_info']['apps'] is not yet set at all. Set it to app => true, it will be used - // in Session->read_repositories() to make sure we get access to these apps when the session loads the apps. - $apps = $GLOBALS['egw']->acl->get_user_applications($share['share_owner']); - $GLOBALS['egw_info']['user']['apps'] = array( - 'filemanager' => $GLOBALS['egw_info']['apps']['filemanager'] || true, - 'collabora' => $GLOBALS['egw_info']['apps']['collabora'] || $apps['collabora'] - ); - - Vfs::$user = $share['share_owner']; - - // Need to re-init stream wrapper, as some of them look at - // preferences or permissions - $scheme = Vfs\StreamWrapper::scheme2class(Vfs::parse_url($share['resolve_url'],PHP_URL_SCHEME)); - if($scheme && method_exists($scheme, 'init_static')) - { - $scheme::init_static(); - } + $class::init_static(); } // mounting share Vfs::$is_root = true; - $clear_fstab = !$keep_session && (!$GLOBALS['egw_info']['user']['account_lid'] || $GLOBALS['egw_info']['user']['account_lid'] == 'anonymous'); - // if current user is not the share owner, we cant just mount share into existing VFS - if ($GLOBALS['egw_info']['user']['account_id'] != $share['share_owner']) - { - $clear_fstab = true; - } - if (!Vfs::mount($share['resolve_url'], $share['share_root'], false, false, $clear_fstab)) + if (!Vfs::mount($share['resolve_url'], $share['share_root'], false, false, true)) { sleep(1); return static::share_fail( @@ -154,6 +174,7 @@ class Sharing extends \EGroupware\Api\Sharing ); } + /* ToDo: is this still needed and for what reason, as Vfs::mount() already supports session / non-persistent mounts $session_fstab =& Api\Cache::getSession('api', 'fstab'); if(!$session_fstab) { @@ -163,8 +184,7 @@ class Sharing extends \EGroupware\Api\Sharing { Vfs::mount($info['mount'], $mount, false, false); } - static::session_mount($share['share_root'], $share['resolve_url']); - + static::session_mount($share['share_root'], $share['resolve_url']);*/ Vfs::$is_root = false; Vfs::clearstatcache(); @@ -190,18 +210,22 @@ class Sharing extends \EGroupware\Api\Sharing ); } - protected function after_login() + protected static function after_login($share) { // only allow filemanager app (gets overwritten by session::create) $GLOBALS['egw_info']['user']['apps'] = array( 'filemanager' => $GLOBALS['egw_info']['apps']['filemanager'] ); // check if sharee has Collabora run rights --> give is to share too - $apps = $GLOBALS['egw']->acl->get_user_applications($this->share['share_owner']); + $apps = $GLOBALS['egw']->acl->get_user_applications($share['share_owner']); if (!empty($apps['collabora'])) { $GLOBALS['egw_info']['user']['apps']['collabora'] = $GLOBALS['egw_info']['apps']['collabora']; } + // session::create also overwrites link-registry + Vfs::clearstatcache(); + // clear link-cache and load link registry without permission check to access /apps + Api\Link::init_static(true); } /** @@ -318,7 +342,7 @@ class Sharing extends \EGroupware\Api\Sharing { if(parse_url($path, PHP_URL_SCHEME) !== 'vfs') { - $path = 'vfs://default'.($path[0] == '/' ? '' : '/').$path; + $path = Vfs::PREFIX.Vfs::parse_url($path, PHP_URL_PATH); } // We don't allow sharing paths that contain links, resolve to target instead @@ -335,7 +359,7 @@ class Sharing extends \EGroupware\Api\Sharing $path = str_replace($check, $delinked, $path); if(parse_url($path, PHP_URL_SCHEME) !== 'vfs') { - $path = 'vfs://default'.($path[0] == '/' ? '' : '/').$path; + $path = Vfs::PREFIX.Vfs::parse_url($path, PHP_URL_PATH); } $check = $path; } @@ -350,8 +374,10 @@ class Sharing extends \EGroupware\Api\Sharing // Make sure we get the correct path if sharing from a share if(isset($GLOBALS['egw']->sharing) && $exists) { + /* Why not use $stat['url'] $resolved_stat = Vfs::parse_url($stat['url']); - $path = 'vfs://default'. $resolved_stat['path']; + $path = 'vfs://default'. $resolved_stat['path'];*/ + $path = $stat['url']; } } } diff --git a/api/src/Vfs/Sharing/StreamWrapper.php b/api/src/Vfs/Sharing/StreamWrapper.php new file mode 100644 index 0000000000..2cadea7234 --- /dev/null +++ b/api/src/Vfs/Sharing/StreamWrapper.php @@ -0,0 +1,124 @@ + + * @copyright (c) 2020 by Ralf Becker + */ + +namespace EGroupware\Api\Vfs\Sharing; + +use EGroupware\Api\Vfs; +use EGroupware\Api; + +/** + * VFS - sharing stream wrapper + * + * Sharing stream wrapper allows to mount a share represented by it's hash and optional password to be mounted + * into EGroupware's VFS: sharing://[:]@default/ --> vfs://@default/ + */ +class StreamWrapper extends Vfs\StreamWrapper +{ + const SCHEME = 'sharing'; + const PREFIX = 'sharing://default'; + + /** + * Method to replace sharing url with sharee and shared path, and to shortcut Vfs\StreamWrapper::resolve_url() + * + * @param $url + * @return bool|string + */ + static function replace($url) + { + $parts = Vfs::parse_url($url); + + $hash = $parts['user'] ?: explode('/', $parts['path'])[1]; + $rel_path = empty($parts['user']) ? preg_replace('|^/[^/]+|', '', $parts['path']) : $parts['path']; + + try + { + if (empty($hash)) throw new Api\Exception\NotFound('Hash must not be empty', 404); + + Api\Sharing::check_token(false, $share, $hash, $parts['pass'] ?? ''); + + return self::share2url($share); + } + catch (Api\Exception $e) { + _egw_log_exception($e); + return false; + } + } + + /** + * Generate sharing URL from share + * + * @param array $share as returned eg. by Api\Sharing::check_token() + * @return string + * @throws Api\Exception\NotFound if sharee was not found + */ + static function share2url(array $share) + { + if (empty($share['share_owner']) || !($account_lid = Api\Accounts::id2name($share['share_owner']))) + { + throw new Api\Exception\NotFound('Share owner not found', 404); + } + return Vfs::concat('vfs://'.$account_lid.'@default'.Vfs::parse_url($share['share_path'], PHP_URL_PATH), $rel_path). + ($share['share_writable'] & 1 ? '' : '?ro=1'); + } + + /** + * Resolve the given path according to our fstab + * + * @param string $url + * @param boolean $do_symlink =true is a direct match allowed, default yes (must be false for a lstat or readlink!) + * @param boolean $use_symlinkcache =true + * @param boolean $replace_user_pass_host =true replace $user,$pass,$host in url, default true, if false result is not cached + * @param boolean $fix_url_query =false true append relativ path to url query parameter, default not + * @return string|boolean false if the url cant be resolved, should not happen if fstab has a root entry + */ + static function resolve_url($url, $do_symlink = true, $use_symlinkcache = true, $replace_user_pass_host = true, $fix_url_query = false) + { + return self::replace($url); + } + + /** + * This method is called in response to stat() calls on the URL paths associated with the wrapper. + * + * Overwritten to set sharee as user in context for ACL checks. + * + * @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! + * @param boolean $try_create_home =false should a user home-directory be created automatic, if it does not exist + * @param boolean $check_symlink_components =true check if path contains symlinks in path components other then the last one + * @return array + */ + function url_stat ( $path, $flags, $try_create_home=false, $check_symlink_components=true, $check_symlink_depth=self::MAX_SYMLINK_DEPTH, $try_reconnect=true ) + { + if (($stat = parent::url_stat($path, $flags, $try_create_home, $check_symlink_components, $check_symlink_depth, $try_reconnect))) + { + $this->check_set_context($stat['url']); + } + return $stat; + } + + /** + * Register __CLASS__ for self::SCHEMA + */ + public static function register() + { + stream_wrapper_register(self::SCHEME, __CLASS__); + } +} + +StreamWrapper::register(); diff --git a/api/src/Vfs/Sqlfs/StreamWrapper.php b/api/src/Vfs/Sqlfs/StreamWrapper.php index ce99232e54..1356546b1c 100644 --- a/api/src/Vfs/Sqlfs/StreamWrapper.php +++ b/api/src/Vfs/Sqlfs/StreamWrapper.php @@ -7,8 +7,7 @@ * @package api * @subpackage vfs * @author Ralf Becker - * @copyright (c) 2008-16 by Ralf Becker - * @version $Id$ + * @copyright (c) 2008-20 by Ralf Becker */ namespace EGroupware\Api\Vfs\Sqlfs; @@ -36,6 +35,8 @@ use EGroupware\Api; */ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface { + use Vfs\UserContextTrait; + /** * Mime type of directories, the old vfs uses 'Directory', while eg. WebDAV uses 'httpd/unix-directory' */ @@ -102,13 +103,6 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface */ 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 * @@ -210,7 +204,7 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface if (!$dir || $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=$this->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 + !$this->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!"); @@ -233,12 +227,12 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface // 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, + 'fs_uid' => $dir_stat['uid'] ? $dir_stat['uid'] : $this->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_creator' => Vfs::$user, // real user, not effective one / $this->user 'fs_mime' => 'application/octet-stream', // required NOT NULL! 'fs_size' => 0, 'fs_active' => self::_pdo_boolean(true), @@ -276,8 +270,8 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface } 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 + if ($mode == 'r' && !$this->check_access($url,Vfs::READABLE , $stat) ||// we are not allowed to read + $mode != 'r' && !$this->check_access($url,Vfs::WRITABLE, $stat)) // or edit it { self::_remove_password($url); $op = $mode == 'r' ? 'read' : 'edited'; @@ -355,7 +349,7 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface // todo: analyse the file for the mime-type 'fs_mime' => Api\MimeMagic::filename2mime($this->opened_path), 'fs_id' => $this->opened_fs_id, - 'fs_modifier' => Vfs::$user, + 'fs_modifier' => $this->user, 'fs_modified' => self::_pdo_timestamp(time()), ); @@ -533,7 +527,7 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface */ function stream_stat ( ) { - if (self::LOG_LEVEL > 1) error_log(__METHOD__."($this->opened_path)"); + if (self::LOG_LEVEL > 1) error_log(__METHOD__."() opened_path=$this->opened_path, context=".json_encode(stream_context_get_options($this->context))); return $this->url_stat($this->opened_path,0); } @@ -558,7 +552,7 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface $this->url_stat($dir, STREAM_URL_STAT_LINK); if (!$parent_stat || !($stat = $this->url_stat($path,STREAM_URL_STAT_LINK)) || - !$dir || !Vfs::check_access($dir, Vfs::WRITABLE, $parent_stat)) + !$dir || !$this->check_access($dir, Vfs::WRITABLE, $parent_stat)) { self::_remove_password($url); if (self::LOG_LEVEL) error_log(__METHOD__."($url) permission denied!"); @@ -610,14 +604,14 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface $to_dir = Vfs::dirname($path_to); if (!($from_stat = $this->url_stat($path_from, 0)) || !$from_dir || - !Vfs::check_access($from_dir, Vfs::WRITABLE, $from_dir_stat = $this->url_stat($from_dir, 0))) + !$this->check_access($from_dir, Vfs::WRITABLE, $from_dir_stat = $this->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 (!$to_dir || !Vfs::check_access($to_dir, Vfs::WRITABLE, $to_dir_stat = $this->url_stat($to_dir, 0))) + if (!$to_dir || !$this->check_access($to_dir, Vfs::WRITABLE, $to_dir_stat = $this->url_stat($to_dir, 0))) { self::_remove_password($url_from); self::_remove_password($url_to); @@ -699,7 +693,7 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface if (self::LOG_LEVEL > 1) error_log(__METHOD__." called from:".function_backtrace()); $path = Vfs::parse_url($url,PHP_URL_PATH); - if ($this->url_stat($path,STREAM_URL_STAT_QUIET)) + if ($this->url_stat($url,STREAM_URL_STAT_QUIET)) { self::_remove_password($url); if (self::LOG_LEVEL) error_log(__METHOD__."('$url',$mode,$options) already exist!"); @@ -733,7 +727,7 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface } $parent = $this->url_stat($parent_path,0); } - if (!$parent || !Vfs::check_access($parent_path,Vfs::WRITABLE,$parent)) + if (!$parent || !$this->check_access($parent_path,Vfs::WRITABLE, $parent)) { self::_remove_password($url); if (self::LOG_LEVEL) error_log(__METHOD__."('$url',$mode,$options) permission denied!"); @@ -796,7 +790,7 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface if (!($parent = Vfs::dirname($path)) || !($stat = $this->url_stat($path, 0)) || $stat['mime'] != self::DIR_MIME_TYPE || - !Vfs::check_access($parent, Vfs::WRITABLE, $this->url_stat($parent,0))) + !$this->check_access($parent, Vfs::WRITABLE)) { self::_remove_password($url); $err_msg = __METHOD__."($url,$options) ".(!$stat ? 'not found!' : @@ -911,7 +905,7 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface return $stmt->execute(array( 'fs_modified' => self::_pdo_timestamp($time ? $time : time()), - 'fs_modifier' => Vfs::$user, + 'fs_modifier' => $this->user, 'fs_id' => $stat['ino'], )); } @@ -1082,7 +1076,7 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface if (!($stat = $this->url_stat($url,0)) || // dir not found !($stat['mode'] & self::MODE_DIR) && $stat['mime'] != self::DIR_MIME_TYPE || // no dir - !Vfs::check_access($url,Vfs::EXECUTABLE|Vfs::READABLE,$stat)) // no access + !$this->check_access($url,Vfs::EXECUTABLE|Vfs::READABLE, $stat)) // no access { self::_remove_password($url); $msg = !($stat['mode'] & self::MODE_DIR) && $stat['mime'] != self::DIR_MIME_TYPE ? @@ -1152,6 +1146,8 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface $path = Vfs::parse_url($url,PHP_URL_PATH); + $this->check_set_context($url); + // webdav adds a trailing slash to dirs, which causes url_stat to NOT find the file otherwise if ($path != '/' && substr($path,-1) == '/') { @@ -1177,7 +1173,7 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface $parts = explode('/',$path); // if we have extended acl access to the url, we dont need and can NOT include the sql for the readable check - $eacl_access = static::check_extended_acl($path,Vfs::READABLE); + $eacl_access = $this->check_extended_acl($path,Vfs::READABLE); try { foreach($parts as $n => $name) @@ -1208,13 +1204,13 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface // 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) + if (!$this->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(); + $query .= ' AND '.$this->_sql_readable(); } } else @@ -1259,23 +1255,23 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface * * @return string */ - protected static function _sql_readable() + protected function _sql_readable() { - static $sql_read_acl=null; + static $sql_read_acl=[]; - if (is_null($sql_read_acl)) + if (!isset($sql_read_acl[$user = $this->user])) { - foreach($GLOBALS['egw']->accounts->memberships(Vfs::$user,true) as $gid) + foreach($GLOBALS['egw']->accounts->memberships($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. + $sql_read_acl[$user] = '((fs_mode & 4)=4 OR (fs_mode & 256)=256 AND fs_uid='.$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())); + //error_log(__METHOD__."() user=".array2string($user).' --> memberships='.array2string($memberships).' --> '.$sql_read_acl.($memberships?'':': '.function_backtrace())); } - return $sql_read_acl; + return $sql_read_acl[$user]; } /** @@ -1341,10 +1337,9 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface * @param string $path * @return string|boolean content of the symlink or false if $url is no symlink (or not found) */ - static function readlink($path) + function readlink($path) { - $vfs = new self(); - $link = !($lstat = $vfs->url_stat($path,STREAM_URL_STAT_LINK)) || is_null($lstat['readlink']) ? false : $lstat['readlink']; + $link = !($lstat = $this->url_stat($path,STREAM_URL_STAT_LINK)) || is_null($lstat['readlink']) ? false : $lstat['readlink']; if (self::LOG_LEVEL > 1) error_log(__METHOD__."('$path') = $link"); @@ -1358,18 +1353,17 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface * @param string $link * @return boolean true on success false on error */ - static function symlink($target,$link) + function symlink($target, $link) { if (self::LOG_LEVEL > 1) error_log(__METHOD__."('$target','$link')"); - $inst = new static(); - if ($inst->url_stat($link,0)) + if ($this->url_stat($link,0)) { if (self::LOG_LEVEL > 0) error_log(__METHOD__."('$target','$link') $link exists, returning false!"); return false; // $link already exists } if (!($dir = Vfs::dirname($link)) || - !Vfs::check_access($dir,Vfs::WRITABLE,$dir_stat=$inst->url_stat($dir,0))) + !$this->check_access($dir,Vfs::WRITABLE, $dir_stat=$this->url_stat($dir,0))) { if (self::LOG_LEVEL > 0) error_log(__METHOD__."('$target','$link') returning false! (!is_writable('$dir'), dir_stat=".array2string($dir_stat).")"); return false; // parent dir does not exist or is not writable @@ -1384,7 +1378,7 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface 'fs_name' => self::limit_filename(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_uid' => $dir_stat['uid'] ? $dir_stat['uid'] : $this->user, 'fs_gid' => $dir_stat['gid'], 'fs_created' => self::_pdo_timestamp(time()), 'fs_modified' => self::_pdo_timestamp(time()), @@ -1407,13 +1401,13 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface * @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) + 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(); + $this->_read_extended_acl(); } $access = false; foreach(self::$extended_acl as $path => $rights) @@ -1432,14 +1426,14 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface * Read the extended acl via acl::get_grants('sqlfs') * */ - static protected function _read_extended_acl() + protected function _read_extended_acl() { if ((self::$extended_acl = Api\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))) + if (($rights = $GLOBALS['egw']->acl->get_all_location_rights($this->user, self::EACL_APPNAME))) { $pathes = self::id2path(array_keys($rights)); } @@ -1843,14 +1837,12 @@ GROUP BY A.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) + function proppatch($path,array $props) { - static $inst = null; if (self::LOG_LEVEL > 1) error_log(__METHOD__."(".array2string($path).','.array2string($props)); if (!is_numeric($path)) { - if (!isset($inst)) $inst = new self(); - if (!($stat = $inst->url_stat($path,0))) + if (!($stat = $this->url_stat($path,0))) { return false; } @@ -1860,7 +1852,7 @@ GROUP BY A.fs_id'; { return false; } - if (!Vfs::check_access($path,Api\Acl::EDIT,$stat)) + if (!$this->check_access($path,Api\Acl::EDIT, $stat)) { return false; // permission denied } @@ -1914,17 +1906,14 @@ GROUP BY A.fs_id'; * @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) + function propfind($path_ids,$ns=Vfs::DEFAULT_PROP_NAMESPACE) { - static $inst = null; - $ids = is_array($path_ids) ? $path_ids : array($path_ids); foreach($ids as &$id) { if (!is_numeric($id)) { - if (!isset($inst)) $inst = new self(); - if (!($stat = $inst->url_stat($id,0))) + if (!($stat = $this->url_stat($id,0))) { if (self::LOG_LEVEL) error_log(__METHOD__."(".array2string($path_ids).",$ns) path '$id' not found!"); return false; @@ -1978,7 +1967,7 @@ GROUP BY A.fs_id'; */ public static function register() { - stream_register_wrapper(self::SCHEME, __CLASS__); + stream_wrapper_register(self::SCHEME, __CLASS__); } } diff --git a/api/src/Vfs/StreamWrapper.php b/api/src/Vfs/StreamWrapper.php index 67ce97bdfe..224fc54f9b 100644 --- a/api/src/Vfs/StreamWrapper.php +++ b/api/src/Vfs/StreamWrapper.php @@ -1,14 +1,13 @@ - * @copyright (c) 2008-16 by Ralf Becker - * @version $Id$ + * @copyright (c) 2008-20 by Ralf Becker */ namespace EGroupware\Api\Vfs; @@ -17,48 +16,26 @@ use EGroupware\Api\Vfs; use EGroupware\Api; /** - * eGroupWare API: VFS - stream wrapper interface + * VFS - stream wrapper * * The new vfs stream wrapper uses a kind of fstab to mount different filesystems / stream wrapper types * together for eGW's virtual file system. * * @link http://www.php.net/manual/en/function.stream-wrapper-register.php */ -class StreamWrapper implements StreamWrapperIface +class StreamWrapper extends Base implements StreamWrapperIface { - /** - * Scheme / protocol used for this stream-wrapper - */ - const SCHEME = 'vfs'; - /** - * Mime type of directories, the old vfs used 'Directory', while eg. WebDAV uses 'httpd/unix-directory' - */ - const DIR_MIME_TYPE = 'httpd/unix-directory'; + use UserContextTrait { + check_access as parent_check_access; + } + + const PREFIX = 'vfs://default'; + /** * Should unreadable entries in a not writable directory be hidden, default yes */ const HIDE_UNREADABLES = true; - /** - * optional context param when opening the stream, null if no context passed - * - * @var mixed - */ - var $context; - /** - * 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!) - */ - const LOG_LEVEL = 1; - /** * Maximum depth of symlinks, if exceeded url_stat will return false * @@ -66,18 +43,6 @@ class StreamWrapper implements StreamWrapperIface */ const MAX_SYMLINK_DEPTH = 10; - /** - * Our fstab in the form mount-point => url - * - * The entry for root has to be the first, or more general if you mount into subdirs the parent has to be before! - * - * @var array - */ - protected static $fstab = array( - '/' => 'sqlfs://$host/', - '/apps' => 'links://$host/apps', - ); - /** * stream / ressouce this class is opened for by stream_open * @@ -146,7 +111,25 @@ class StreamWrapper implements StreamWrapperIface */ private $extra_dir_ptr; - private static $wrappers; + /** + * 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 = Vfs::READABLE, + * 2 = Vfs::WRITABLE, 1 = Vfs::EXECUTABLE + * @param array|boolean $stat =null stat array or false, to not query it again + * @return boolean + */ + function check_access($path, $check, $stat=null) + { + $ret = self::_call_on_backend('check_access', [$path, $check, $stat], "null", 0, true); + if (!isset($ret)) + { + $ret = $this->parent_check_access($path, $check, $stat); + } + return $ret; + } /** * Resolve the given path according to our fstab AND symlinks @@ -174,119 +157,17 @@ class StreamWrapper implements StreamWrapperIface // if the url resolves to a symlink to the vfs, resolve this vfs:// url direct if ($url && Vfs::parse_url($url,PHP_URL_SCHEME) == self::SCHEME) { + $user = Vfs::parse_url($url,PHP_URL_USER); $url = self::resolve_url(Vfs::parse_url($url,PHP_URL_PATH)); + if (!empty($user) && empty(parse_url($url, PHP_URL_USER))) + { + $url = str_replace('://', '://'.$user.'@', $url); + } } if (self::LOG_LEVEL > 1) error_log(__METHOD__."($path,file_exists=$file_exists,resolve_last_symlink=$resolve_last_symlink) = '$url'$log"); return $url; } - /** - * Cache of already resolved urls - * - * @var array with path => target - */ - private static $resolve_url_cache = array(); - - /** - * Resolve the given path according to our fstab - * - * @param string $_path - * @param boolean $do_symlink =true is a direct match allowed, default yes (must be false for a lstat or readlink!) - * @param boolean $use_symlinkcache =true - * @param boolean $replace_user_pass_host =true replace $user,$pass,$host in url, default true, if false result is not cached - * @param boolean $fix_url_query =false true append relativ path to url query parameter, default not - * @return string|boolean false if the url cant be resolved, should not happen if fstab has a root entry - */ - static function resolve_url($_path,$do_symlink=true,$use_symlinkcache=true,$replace_user_pass_host=true,$fix_url_query=false) - { - $path = self::get_path($_path); - - // we do some caching here - if (isset(self::$resolve_url_cache[$path]) && $replace_user_pass_host) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__."('$path') = '".self::$resolve_url_cache[$path]."' (from cache)"); - return self::$resolve_url_cache[$path]; - } - // check if we can already resolve path (or a part of it) with a known symlinks - if ($use_symlinkcache) - { - $path = self::symlinkCache_resolve($path,$do_symlink); - } - // setting default user, passwd and domain, if it's not contained int the url - $defaults = array( - 'user' => $GLOBALS['egw_info']['user']['account_lid'], - 'pass' => urlencode($GLOBALS['egw_info']['user']['passwd']), - 'host' => $GLOBALS['egw_info']['user']['domain'], - 'home' => str_replace(array('\\\\','\\'),array('','/'),$GLOBALS['egw_info']['user']['homedirectory']), - ); - $parts = array_merge(Vfs::parse_url($path),$defaults); - if (!$parts['host']) $parts['host'] = 'default'; // otherwise we get an invalid url (scheme:///path/to/something)! - - if (!empty($parts['scheme']) && $parts['scheme'] != self::SCHEME) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__."('$path') = '$path' (path is already an url)"); - return $path; // path is already a non-vfs url --> nothing to do - } - if (empty($parts['path'])) $parts['path'] = '/'; - - foreach(array_reverse(self::$fstab) as $mounted => $url) - { - if ($mounted == '/' || $mounted == $parts['path'] || $mounted.'/' == substr($parts['path'],0,strlen($mounted)+1)) - { - $scheme = Vfs::parse_url($url,PHP_URL_SCHEME); - if (is_null(self::$wrappers) || !in_array($scheme,self::$wrappers)) - { - self::load_wrapper($scheme); - } - if (($relative = substr($parts['path'],strlen($mounted)))) - { - $url = Vfs::concat($url,$relative); - } - // if url contains url parameter, eg. from filesystem streamwrapper, we need to append relative path here too - $matches = null; - if ($fix_url_query && preg_match('|([?&]url=)([^&]+)|', $url, $matches)) - { - $url = str_replace($matches[0], $matches[1].Vfs::concat($matches[2], substr($parts['path'],strlen($mounted))), $url); - } - - if ($replace_user_pass_host) - { - $url = str_replace(array('$user','$pass','$host','$home'),array($parts['user'],$parts['pass'],$parts['host'],$parts['home']),$url); - } - if ($parts['query']) $url .= '?'.$parts['query']; - if ($parts['fragment']) $url .= '#'.$parts['fragment']; - - if (self::LOG_LEVEL > 1) error_log(__METHOD__."('$path') = '$url'"); - - if ($replace_user_pass_host) self::$resolve_url_cache[$path] = $url; - - return $url; - } - } - if (self::LOG_LEVEL > 0) error_log(__METHOD__."('$path') can't resolve path!\n"); - trigger_error(__METHOD__."($path) can't resolve path!\n",E_USER_WARNING); - return false; - } - - /** - * Returns mount url of a full url returned by resolve_url - * - * @param string $fullurl full url returned by resolve_url - * @return string|NULL mount url or null if not found - */ - static function mount_url($fullurl) - { - foreach(array_reverse(self::$fstab) as $url) - { - list($url_no_query) = explode('?',$url); - if (substr($fullurl,0,1+strlen($url_no_query)) === $url_no_query.'/') - { - return $url; - } - } - return null; - } - /** * This method is called immediately after your stream object is created. * @@ -313,6 +194,8 @@ class StreamWrapper implements StreamWrapperIface { return false; } + $this->check_set_context($url); + if (!($this->opened_stream = $this->context ? fopen($url, $mode, false, $this->context) : fopen($url, $mode, false))) { @@ -325,7 +208,7 @@ class StreamWrapper implements StreamWrapperIface // are we requested to treat the opened file as new file (only for files opened NOT for reading) if ($mode[0] != 'r' && !$this->opened_stream_is_new && $this->context && - ($opts = stream_context_get_params($this->context)) && + ($opts = stream_context_get_options($this->context)) && $opts['options'][self::SCHEME]['treat_as_new']) { $this->opened_stream_is_new = true; @@ -538,10 +421,12 @@ class StreamWrapper implements StreamWrapperIface { return false; } + // set user-context + $this->check_set_context($url); $stat = $this->url_stat($path, STREAM_URL_STAT_LINK); self::symlinkCache_remove($path); - $ok = unlink($url); + $ok = unlink($url, $this->context); // call "vfs_unlink" hook only after successful unlink, with data from (not longer possible) stat call if ($ok && !class_exists('setup_process', false)) @@ -585,13 +470,16 @@ class StreamWrapper implements StreamWrapperIface { return false; } + // set user-context + $this->check_set_context($url_from); + // if file is moved from one filesystem / wrapper to an other --> copy it (rename fails cross wrappers) if (Vfs::parse_url($url_from,PHP_URL_SCHEME) == Vfs::parse_url($url_to,PHP_URL_SCHEME)) { self::symlinkCache_remove($path_from); - $ret = rename($url_from,$url_to); + $ret = rename($url_from, $url_to, $this->context); } - elseif (($from = fopen($url_from,'r')) && ($to = fopen($url_to,'w'))) + elseif (($from = fopen($url_from,'r', false, $this->context)) && ($to = fopen($url_to,'w'))) { $ret = stream_copy_to_stream($from,$to) !== false; fclose($from); @@ -642,6 +530,11 @@ class StreamWrapper implements StreamWrapperIface { return false; } + // set user context + if (Vfs::parse_url($url, PHP_URL_USER)) + { + $this->check_set_context($url); + } // check if recursive option is set and needed if (($options & STREAM_MKDIR_RECURSIVE) && ($parent_url = Vfs::dirname($url)) && @@ -660,7 +553,7 @@ class StreamWrapper implements StreamWrapperIface $options &= ~STREAM_MKDIR_RECURSIVE; } - $ret = mkdir($url,$mode,$options); + $ret = mkdir($url, $mode, $options, $this->context); // call "vfs_mkdir" hook if ($ret && !class_exists('setup_process', false)) @@ -702,8 +595,14 @@ class StreamWrapper implements StreamWrapperIface } $stat = $this->url_stat($path, STREAM_URL_STAT_LINK); + // set user context + if (Vfs::parse_url($url, PHP_URL_USER)) + { + $this->check_set_context($url); + } self::symlinkCache_remove($path); - $ok = rmdir($url); + $ok = rmdir($url, $this->context); + clearstatcache(); // otherwise next stat call still returns it // call "vfs_rmdir" hook, only after successful rmdir if ($ok && !class_exists('setup_process', false)) @@ -735,13 +634,16 @@ class StreamWrapper implements StreamWrapperIface if (self::LOG_LEVEL > 0) error_log(__METHOD__."( $path,$options) resolve_url_symlinks() failed!"); return false; } + // need to set user-context from resolved url + $this->check_set_context($this->opened_dir_url); + if (!($this->opened_dir = $this->context ? opendir($this->opened_dir_url, $this->context) : opendir($this->opened_dir_url))) { if (self::LOG_LEVEL > 0) error_log(__METHOD__."( $path,$options) opendir($this->opened_dir_url) failed!"); return false; } - $this->opened_dir_writable = Vfs::check_access($this->opened_dir_url,Vfs::WRITABLE); + $this->opened_dir_writable = $this->check_access($this->opened_dir_url,Vfs::WRITABLE); // check our fstab if we need to add some of the mountpoints $basepath = Vfs::parse_url($path,PHP_URL_PATH); foreach(array_keys(self::$fstab) as $mounted) @@ -749,7 +651,7 @@ class StreamWrapper implements StreamWrapperIface 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 || - Vfs::check_access($mounted,Vfs::READABLE))) + $this->check_access($mounted,Vfs::READABLE))) { $this->extra_dirs[] = Vfs::basename($mounted); } @@ -790,12 +692,30 @@ class StreamWrapper implements StreamWrapperIface */ function url_stat ( $path, $flags, $try_create_home=false, $check_symlink_components=true, $check_symlink_depth=self::MAX_SYMLINK_DEPTH, $try_reconnect=true ) { - if (!($url = self::resolve_url($path,!($flags & STREAM_URL_STAT_LINK), $check_symlink_components))) + // we have no context, but $path is a URL with a valid user --> set it + $this->check_set_context($path); + + if (!($url = static::resolve_url($path, !($flags & STREAM_URL_STAT_LINK), $check_symlink_components, true, false, $mount_point))) { if (self::LOG_LEVEL > 0) error_log(__METHOD__."('$path',$flags) can NOT resolve path!"); return false; } + // we need to make sure the mount-point is readable eg. if something is mounted into an other users home-directory + if (!isset($mount_point)) Vfs::mount_url($url, $mount_point); // resolve_url only returns mount-point for pathes or vfs urls + if (!($mount_point === '/' || Vfs::dirname($mount_point) === '/') && // they all are public readable + ($class = self::scheme2class(Vfs::parse_url($url, PHP_URL_SCHEME))) && + !is_a($class, Vfs\Sqlfs\StreamWrapper::class) && // decendents of SqlFS stream-wrapper always check traversal right to / + !$this->check_access(Vfs::dirname($mount_point), Vfs::READABLE)) + { + return false; // mount-point is not reachable + } + + if (empty(parse_url($url, PHP_URL_USER))) + { + $url = str_replace('://', '://'.Api\Accounts::id2name($this->context ? stream_context_get_options($this->context)[self::SCHEME]['user'] : Vfs::$user).'@', $url); + } + try { if ($flags & STREAM_URL_STAT_LINK) { @@ -901,6 +821,47 @@ class StreamWrapper implements StreamWrapperIface return $stat;*/ } + /** + * 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 $path path to check + * @param int $check mode to check: one or more or'ed together of: 4 = read, 2 = write, 1 = executable + * @return boolean + */ + function check_extended_acl($path, $check) + { + if (!($url = self::resolve_url($path))) + { + if (self::LOG_LEVEL > 0) error_log(__METHOD__."('$path', $check) can NOT resolve path: ".function_backtrace(1)); + return false; + } + // check backend for extended acls (only if path given) + return self::_call_on_backend('check_extended_acl', [$url, $check], true, 0, true); // true = fail silent if backend does not support + } + + /** + * 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 + */ + function has_owner_rights($path,array $stat=null) + { + if (!$stat) + { + $stat = $this->url_stat($path,0); + } + return $stat['uid'] == $this->user && // (current) user is the owner + Vfs::$is_root || // class runs with root rights + !$stat['uid'] && $stat['gid'] && Vfs::$is_admin; // group directory and user is an eGW admin + } + /** * Check if path (which fails the stat call) contains symlinks in path-components other then the last one * @@ -945,95 +906,6 @@ class StreamWrapper implements StreamWrapperIface return false; // $path does not exist } - /** - * Cache of already resolved symlinks - * - * @var array with path => target - */ - private static $symlink_cache = array(); - - /** - * Add a resolved symlink to cache - * - * @param string $_path vfs path - * @param string $target target path - */ - static protected function symlinkCache_add($_path,$target) - { - $path = self::get_path($_path); - - if (isset(self::$symlink_cache[$path])) return; // nothing to do - - if ($target[0] != '/') $target = Vfs::parse_url($target,PHP_URL_PATH); - - self::$symlink_cache[$path] = $target; - - // sort longest path first - uksort(self::$symlink_cache, function($b, $a) - { - return strlen($a) - strlen($b); - }); - if (self::LOG_LEVEL > 1) error_log(__METHOD__."($path,$target) cache now ".array2string(self::$symlink_cache)); - } - - /** - * Remove a resolved symlink from cache - * - * @param string $_path vfs path - */ - static public function symlinkCache_remove($_path) - { - $path = self::get_path($_path); - - unset(self::$symlink_cache[$path]); - if (self::LOG_LEVEL > 1) error_log(__METHOD__."($path) cache now ".array2string(self::$symlink_cache)); - } - - /** - * Resolve a path from our symlink cache - * - * The cache is sorted from longer to shorter pathes. - * - * @param string $_path - * @param boolean $do_symlink =true is a direct match allowed, default yes (must be false for a lstat or readlink!) - * @return string target or path, if path not found - */ - static public function symlinkCache_resolve($_path,$do_symlink=true) - { - // remove vfs scheme, but no other schemes (eg. filesystem!) - $path = self::get_path($_path); - - $strlen_path = strlen($path); - - foreach(self::$symlink_cache as $p => $t) - { - if (($strlen_p = strlen($p)) > $strlen_path) continue; // $path can NOT start with $p - - if ($path == $p) - { - if ($do_symlink) $target = $t; - break; - } - elseif (substr($path,0,$strlen_p+1) == $p.'/') - { - $target = $t . substr($path,$strlen_p); - break; - } - } - if (self::LOG_LEVEL > 1 && isset($target)) error_log(__METHOD__."($path) = $target"); - return isset($target) ? $target : $path; - } - - /** - * Clears our internal stat and symlink cache - * - * Normaly not necessary, as it is automatically cleared/updated, UNLESS Vfs::$user changes! - */ - static function clearstatcache() - { - self::$symlink_cache = self::$resolve_url_cache = array(); - } - /** * This method is called in response to readdir(). * @@ -1059,7 +931,7 @@ class StreamWrapper implements StreamWrapperIface 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 && - !Vfs::check_access(Vfs::concat($this->opened_dir_url,$file),Vfs::READABLE))); + !$this->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; @@ -1096,230 +968,6 @@ class StreamWrapper implements StreamWrapperIface return $ret; } - /** - * Load stream wrapper for a given schema - * - * @param string $scheme - * @return boolean - */ - static function load_wrapper($scheme) - { - if (!in_array($scheme,self::get_wrappers())) - { - switch($scheme) - { - case 'webdav': - case 'webdavs': - require_once('HTTP/WebDAV/Client.php'); - self::$wrappers[] = $scheme; - break; - case '': - break; // default file, always loaded - default: - // check if scheme is buildin in php or one of our own stream wrappers - if (in_array($scheme,stream_get_wrappers()) || class_exists(self::scheme2class($scheme))) - { - self::$wrappers[] = $scheme; - } - else - { - trigger_error("Can't load stream-wrapper for scheme '$scheme'!",E_USER_WARNING); - return false; - } - } - } - return true; - } - - /** - * Return already loaded stream wrappers - * - * @return array - */ - static function get_wrappers() - { - if (is_null(self::$wrappers)) - { - self::$wrappers = stream_get_wrappers(); - } - return self::$wrappers; - } - - /** - * Get the class-name for a scheme - * - * A scheme is not allowed to contain an underscore, but allows a dot and a class names only allow or need underscores, but no dots - * --> we replace dots in scheme with underscored to get the class-name - * - * @param string $scheme eg. vfs - * @return string - */ - static function scheme2class($scheme) - { - 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; - } - } - - /** - * Getting the path from an url (or path) AND removing trailing slashes - * - * @param string $path url or path (might contain trailing slash from WebDAV!) - * @param string $only_remove_scheme =self::SCHEME if given only that scheme get's removed - * @return string path without training slash - */ - static protected function get_path($path,$only_remove_scheme=self::SCHEME) - { - if ($path[0] != '/' && (!$only_remove_scheme || Vfs::parse_url($path, PHP_URL_SCHEME) == $only_remove_scheme)) - { - $path = Vfs::parse_url($path, PHP_URL_PATH); - } - // remove trailing slashes eg. added by WebDAV, but do NOT remove / from "sqlfs://default/"! - if ($path != '/') - { - while (mb_substr($path, -1) == '/' && $path != '/' && ($path[0] == '/' || Vfs::parse_url($path, PHP_URL_PATH) != '/')) - { - $path = mb_substr($path,0,-1); - } - } - return $path; - } - - /** - * Check if url contains ro=1 parameter to mark mount readonly - * - * @param string $url - * @return boolean - */ - static function url_is_readonly($url) - { - static $cache = array(); - $ret =& $cache[$url]; - if (!isset($ret)) - { - $matches = null; - $ret = preg_match('/\?(.*&)?ro=([^&]+)/', $url, $matches) && $matches[2]; - } - return $ret; - } - - /** - * 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 = Api\Config::read('phpgwapi'); - if (isset($api_config['vfs_fstab']) && is_array($api_config['vfs_fstab'])) - { - self::$fstab = $api_config['vfs_fstab']; - } - else - { - self::$fstab = array( - '/' => 'sqlfs://$host/', - '/apps' => 'links://$host/apps', - ); - } - unset($api_config); - } - if (is_null($url) || is_null($path)) - { - if (self::LOG_LEVEL > 1) error_log(__METHOD__.'('.array2string($url).','.array2string($path).') returns '.array2string(self::$fstab)); - return self::$fstab; - } - if (!Vfs::$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(Vfs::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, function($a, $b) - { - return strlen($a) - strlen($b); - }); - - if ($persitent_mount) - { - Api\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 (!Vfs::$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]); - - Api\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; - } - /** * Init our static properties and register this wrapper * @@ -1329,12 +977,19 @@ class StreamWrapper implements StreamWrapperIface if (in_array(self::SCHEME, stream_get_wrappers())) { stream_wrapper_unregister(self::SCHEME); } - stream_register_wrapper(self::SCHEME,__CLASS__); + stream_wrapper_register(self::SCHEME,__CLASS__); if (($fstab = $GLOBALS['egw_info']['server']['vfs_fstab']) && is_array($fstab) && count($fstab)) { self::$fstab = $fstab; } + + // set default context for our schema ('vfs') with current user + if (!($context = stream_context_get_options(stream_context_get_default())) || empty($context[self::SCHEME]['user'])) + { + $context[self::SCHEME]['user'] = (int)$GLOBALS['egw_info']['user']['account_id']; + stream_context_set_default($context); + } } } diff --git a/api/src/Vfs/UserContextTrait.php b/api/src/Vfs/UserContextTrait.php new file mode 100644 index 0000000000..b57a3b12e9 --- /dev/null +++ b/api/src/Vfs/UserContextTrait.php @@ -0,0 +1,247 @@ + + * @copyright (c) 2020 by Ralf Becker + */ + +namespace EGroupware\Api\Vfs; + +use EGroupware\Api\Vfs; +use EGroupware\Api; + +/** + * Trait to store user / account_id in stream context + * + * Used by Vfs and SqlFS stream-wrapper. + * + * @property int $user user / account_id stored in context + */ +trait UserContextTrait +{ + /** + * optional context param when opening the stream, null if no context passed + * + * @var resource + */ + public $context; + + /** + * Contructor to set context/user incl. from user in url or passed in context + * + * @param resource|string|null $url_or_context url with user or context to set + */ + public function __construct($url_or_context=null) + { + if (is_resource($url_or_context)) + { + $this->context = $url_or_context; + } + else + { + if (!isset($this->context)) // PHP set's it before constructor is called! + { + $this->context = stream_context_get_default(); + } + // if context set by PHP contains no user, set user from our default context (Vfs::$user) + elseif (empty(stream_context_get_options($this->context)[Vfs::SCHEME]['user'])) + { + stream_context_set_option($this->context, stream_context_get_options(stream_context_get_default())); + } + + if (is_string($url_or_context)) + { + $this->check_set_context($url_or_context); + } + } + } + + /** + * Check if we have an url with a user --> set it as context + * + * @param $url + */ + protected function check_set_context($url) + { + if ($url[0] !== '/' && ($account_lid = Vfs::parse_url($url, PHP_URL_USER))) + { + $this->user = $account_lid; + } + } + + /** + * 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 = Vfs::READABLE, + * 2 = Vfs::WRITABLE, 1 = Vfs::EXECUTABLE + * @param array|boolean $stat =null stat array or false, to not query it again + * @return boolean + */ + function check_access($path, $check, $stat=null) + { + if (Vfs::$is_root) + { + return true; + } + + // throw exception if stat array is used insead of path, can be removed soon + if (is_array($path)) + { + throw new Exception\WrongParameter('path has to be string, use check_access($path,$check,$stat=null)!'); + } + + // if we have no $stat, delegate whole check to vfs stream-wrapper to correctly deal with shares / effective user-ids + if (is_null($stat)) + { + $stat = $this->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 + } + + // only vfs stream-wrapper sets $stat['url'], use given url instead + if (!isset($stat['url']) && $path[0] !== '/') + { + $stat['url'] = $path; + } + + // if we check writable and have a readonly mount --> return false, as backends dont know about r/o url parameter + if ($check == Vfs::WRITABLE && Vfs\StreamWrapper::url_is_readonly($stat['url'])) + { + //error_log(__METHOD__."(path=$path, check=writable, ...) failed because mount is readonly"); + return false; + } + + // 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 = Vfs::parse_url($stat['url'], PHP_URL_SCHEME)) && !(class_exists(Vfs::scheme2class($scheme)))) + { + switch($check) + { + case Vfs::READABLE: + return is_readable($stat['url']); + case Vfs::WRITABLE: + return is_writable($stat['url']); + case Vfs::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'] == $this->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 = Api\Accounts::getInstance()->memberships($this->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 extended acls (only if path given) + $ret = method_exists($this, 'check_extended_acl') && $path && $this->check_extended_acl($stat['url'] ?? $path, $check); + + //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; + } + + /** + * @param string $name + * @return mixed|null + */ + public function __get($name) + { + switch($name) + { + case 'user': + return $this->context ? stream_context_get_options($this->context)[Vfs::SCHEME]['user'] : Vfs::$user; + } + return null; + } + + /** + * @param string $name + * @param mixed $value + */ + public function __set($name, $value) + { + switch($name) + { + case 'user': + if (!is_int($value) && is_string($value) && !is_numeric($value)) + { + $value = Api\Accounts::getInstance()->name2id($value); + } + if ($value) + { + $options = [ + Vfs::SCHEME => ['user' => (int)$value] + ]; + // do NOT overwrite default context + if ($this->context && $this->context !== stream_context_get_default()) + { + stream_context_set_option($this->context, $options); + } + else + { + $this->context = stream_context_create($options); + } + } + break; + } + } + + /** + * Get user context for given url, eg. to use with regular stream functions + * + * @param string $url + * @param array $extra =[] addtional context options + * @return resource context with user, plus optional extra options + */ + public static function userContext($url, array $extra=[]) + { + if (($user = Vfs::parse_url($url, PHP_URL_USER)) && + ($account_id = Api\Accounts::getInstance()->name2id($user)) && + ($account_id != Vfs::$user) || $extra) // never set extra options on default context! + { + $context = stream_context_create(array_merge_recursive([Vfs::SCHEME => ['user' => (int)$account_id ?: Vfs::$user]], $extra)); + } + else + { + $context = stream_context_get_default(); + } + return $context; + } + + /** + * @param string $name + * @return bool + */ + public function __isset($name) + { + return $this->__get($name) !== null; + } +} diff --git a/api/tests/LoggedInTest.php b/api/tests/LoggedInTest.php index 0395df39ec..b4ac0df9fc 100644 --- a/api/tests/LoggedInTest.php +++ b/api/tests/LoggedInTest.php @@ -74,6 +74,14 @@ abstract class LoggedInTest extends TestCase */ public static function tearDownAfterClass() : void { + // Clean up VFS + Vfs::clearstatcache(); + // Reset stream context, or current user will always be there + stream_context_set_option(stream_context_get_default(),['vfs'=>['user' => null]]); + + // Clear some link caching + Link::init_static(true); + if($GLOBALS['egw']) { if($GLOBALS['egw']->session) @@ -181,6 +189,9 @@ abstract class LoggedInTest extends TestCase // Disable asyc while we test $GLOBALS['egw_info']['server']['asyncservice'] = 'off'; + // Set up Vfs + Vfs::init_static(); + Vfs\StreamWrapper::init_static(); while(ob_get_level() > $ob_level) { ob_end_flush(); @@ -228,4 +239,19 @@ abstract class LoggedInTest extends TestCase return true; } + + /** + * Log out the current user, log in as the given user + * + * @param $account_lid + * @param $password + */ + protected function switchUser($account_lid, $password) + { + // Log out + self::tearDownAfterClass(); + + // Log in + static::load_egw($account_lid,$password); + } } \ No newline at end of file diff --git a/api/tests/Vfs/Filesystem/StreamWrapperTest.php b/api/tests/Vfs/Filesystem/StreamWrapperTest.php new file mode 100644 index 0000000000..b35dcb11be --- /dev/null +++ b/api/tests/Vfs/Filesystem/StreamWrapperTest.php @@ -0,0 +1,58 @@ +files[] = $this->test_file = $this->getFilename(); + } + + protected function tearDown() : void + { + parent::tearDown(); + } + + protected function mount(): void + { + $this->mountFilesystem(static::$mountpoint); + } + + protected function allowAccess(string $test_name, string &$test_file, int $test_user, string $needed) : void + { + // We'll allow access by putting test user in Default group + $command = new \admin_cmd_edit_user($test_user, ['account_groups' => array_merge($this->account['account_groups'],['Default'])]); + $command->run(); + + // Add explicit permission on group + Vfs::chmod($test_file, Vfs::mode2int('g+'.$needed)); + } + + /** + * Make a filename that reflects the current test + */ + protected function getFilename($path = null) + { + return parent::getFilename(static::$mountpoint); + } +} \ No newline at end of file diff --git a/api/tests/Vfs/Links/StreamWrapperTest.php b/api/tests/Vfs/Links/StreamWrapperTest.php new file mode 100644 index 0000000000..502d709493 --- /dev/null +++ b/api/tests/Vfs/Links/StreamWrapperTest.php @@ -0,0 +1,113 @@ +entries as $entry) + { + $bo->delete($entry); + } + + parent::tearDown(); + } + + public function testSimpleReadWrite(): string + { + $info_id = $this->make_infolog(); + $this->files[] = $this->test_file = $this->getFilename(null, $info_id); + + return parent::testSimpleReadWrite(); + } + + public function testNoReadAccess(): void + { + $info_id = $this->make_infolog(); + $this->files[] = $this->test_file = $this->getFilename(null, $info_id); + + parent::testNoReadAccess(); + } + + public function testWithAccess(): void + { + $info_id = $this->make_infolog(); + $this->files[] = $this->test_file = $this->getFilename(null, $info_id); + + parent::testWithAccess(); + } + + protected function allowAccess(string $test_name, string &$test_file, int $test_user, string $needed) : void + { + // We'll allow access by putting test user in responsible + $so = new \infolog_so(); + $element = $so->read(Array('info_id' => $this->entries[0])); + $element['info_responsible'] = [$test_user]; + $so->write($element); + } + + protected function mount() : void + { + $this->mountLinks('/apps'); + } + + /** + * Make an infolog entry + */ + protected function make_infolog() + { + $bo = new \infolog_bo(); + $element = array( + 'info_subject' => "Test infolog for #{$this->getName()}", + 'info_des' => 'Test element for ' . $this->getName() . "\n" . Api\DateTime::to(), + 'info_status' => 'open' + ); + + $element_id = $bo->write($element, true, true, true, true); + $this->entries[] = $element_id; + return $element_id; + } + + /** + * Make a filename that reflects the current test + * @param $path + * @param $info_id + * @return string + * @throws \ReflectionException + */ + protected function getFilename($path, $info_id) + { + if(is_null($path)) $path = '/apps/infolog/'; + if(substr($path,-1,1) !== '/') $path = $path . '/'; + $reflect = new \ReflectionClass($this); + return $path .$info_id .'/'. $reflect->getShortName() . '_' . $this->getName() . '.txt'; + } + +} \ No newline at end of file diff --git a/api/tests/Vfs/ProppatchTest.php b/api/tests/Vfs/ProppatchTest.php index b44b635c6f..905fc61619 100644 --- a/api/tests/Vfs/ProppatchTest.php +++ b/api/tests/Vfs/ProppatchTest.php @@ -19,7 +19,7 @@ use EGroupware\Api\Vfs; use EGroupware\Stylite\Vfs\Versioning; -class ProppatchTest extends StreamWrapperBase +class ProppatchTest extends LoggedInTest { protected function setUp() : void { @@ -113,4 +113,17 @@ class ProppatchTest extends StreamWrapperBase ); } + + /** + * Make a filename that reflects the current test + */ + protected function getFilename($path = null) + { + if(is_null($path)) $path = Vfs::get_home_dir().'/'; + if(substr($path,-1,1) !== '/') $path = $path . '/'; + + $reflect = new \ReflectionClass($this); + return $path . $reflect->getShortName() . '_' . $this->getName(false) . '.txt'; + } + } \ No newline at end of file diff --git a/api/tests/Vfs/Sharing/StreamWrapperTest.php b/api/tests/Vfs/Sharing/StreamWrapperTest.php new file mode 100644 index 0000000000..d15388837e --- /dev/null +++ b/api/tests/Vfs/Sharing/StreamWrapperTest.php @@ -0,0 +1,117 @@ +createShare(); + parent::setUp(); + } + + protected function tearDown() : void + { + parent::tearDown(); + } + + public function testSimpleReadWrite(): string + { + $this->files[] = $this->test_file = $this->getFilename('',false); + + return parent::testSimpleReadWrite(); + } + + public function testNoReadAccess(): void + { + $this->files[] = $this->test_file = $this->getFilename('',false); + + parent::testNoReadAccess(); + } + + public function testWithAccess(): void + { + $this->files[] = $this->test_file = $this->getFilename('',false); + + parent::testWithAccess(); + } + + protected function allowAccess(string $test_name, string &$test_file, int $test_user, string $needed) : void + { + // Anyone who mounts will have access, but the available path changes + $test_file = '/home/'. $GLOBALS['egw']->accounts->id2name($test_user) . '/' . + Vfs\Sharing::SHARES_DIRECTORY .'/'.static::$test_dir .'/'. Vfs::basename($test_file); + } + + public function mount() : void + { + Api\Vfs\Sharing::setup_share(true,$this->share); + } + + public function createShare(&$dir='', $extra = array(), $create = 'createShare') + { + // First, create the directory to be shared + $this->files[] = $dir = Vfs::get_home_dir() . '/'. static::$test_dir; + Vfs::mkdir($dir); + + // Create and use link + $this->getShareExtra($dir, Sharing::WRITABLE, $extra); + + $this->share = Vfs\Sharing::create('',$dir,Sharing::WRITABLE,$dir,'',$extra); + $link = Vfs\Sharing::share2link($this->share); + + return $link; + } + + + /** + * Get the extra information required to create a share link for the given + * directory, with the given mode + * + * @param string $dir Share target + * @param int $mode Share mode + * @param Array $extra + */ + protected function getShareExtra($dir, $mode, &$extra) + { + switch($mode) + { + case Sharing::WRITABLE: + $extra['share_writable'] = TRUE; + break; + } + } + + /** + * Make a filename that reflects the current test + * @param $path + * @param bool $mounted Get the path if the share is mounted, or the original + * @return string + */ + protected function getFilename($path = null, $mounted = true) : string + { + return parent::getFilename(Vfs::get_home_dir() . '/'. + ($mounted ? Vfs\Sharing::SHARES_DIRECTORY .'/' : '').static::$test_dir .'/'. $path); + } + +} \ No newline at end of file diff --git a/api/tests/Vfs/SharingACLTest.php b/api/tests/Vfs/SharingACLTest.php index d5c01ebf52..7267266864 100644 --- a/api/tests/Vfs/SharingACLTest.php +++ b/api/tests/Vfs/SharingACLTest.php @@ -55,6 +55,7 @@ class SharingACLTest extends SharingBase protected function tearDown() : void { + LoggedInTest::setUpBeforeClass(); parent::tearDown(); if($this->account_id) { @@ -198,9 +199,6 @@ class SharingACLTest extends SharingBase $this->assertNotNull($form, "Could not read the share link"); $rows = $data['data']['content']['nm']['rows']; - Vfs::clearstatcache(); - Vfs::init_static(); - Vfs\StreamWrapper::init_static(); // Check we can't find the non-shared file $result = array_filter($rows, function($v) { @@ -236,10 +234,6 @@ class SharingACLTest extends SharingBase $this->assertNotNull($form, "Could not read the share link"); $rows = array_values($data['data']['content']['nm']['rows']); - Vfs::clearstatcache(); - Vfs::init_static(); - Vfs\StreamWrapper::init_static(); - // Check we can't find the non-shared file $result = array_filter($rows, function($v) { return $v['name'] == $this->no_access; @@ -318,6 +312,6 @@ class SharingACLTest extends SharingBase // Log out & clear cache LoggedInTest::tearDownAfterClass(); - $this->checkSharedFile($link, $mimetype); + $this->checkSharedFile($link, $mimetype, $share); } } diff --git a/api/tests/Vfs/SharingBackendTest.php b/api/tests/Vfs/SharingBackendTest.php index 60b598b82b..5da33932a8 100644 --- a/api/tests/Vfs/SharingBackendTest.php +++ b/api/tests/Vfs/SharingBackendTest.php @@ -31,7 +31,7 @@ class SharingBackendTest extends SharingBase */ public function testHomeReadonly() { - $dir = Vfs::get_home_dir().'/'; + $dir = Vfs::get_home_dir().'/'.$this->getName(false).'/'; $this->checkDirectory($dir, Sharing::READONLY); } @@ -42,7 +42,7 @@ class SharingBackendTest extends SharingBase */ public function testHomeWritable() { - $dir = Vfs::get_home_dir().'/'; + $dir = Vfs::get_home_dir().'/'.$this->getName(false).'/'; $this->checkDirectory($dir, Sharing::WRITABLE); } @@ -124,6 +124,9 @@ class SharingBackendTest extends SharingBase */ public function testLinksReadonly() { + // Need to mount apps + $this->mountLinks("/apps"); + // Create an infolog entry for testing purposes $info_id = $this->make_infolog(); $bo = new \infolog_bo(); @@ -142,6 +145,9 @@ class SharingBackendTest extends SharingBase */ public function testLinksWritable() { + // Need to mount apps + $this->mountLinks("/apps"); + // Create an infolog entry for testing purposes $bo = new \infolog_bo(); $info_id = $this->make_infolog(); diff --git a/api/tests/Vfs/SharingBase.php b/api/tests/Vfs/SharingBase.php index 64c84da840..6e50b1d41a 100644 --- a/api/tests/Vfs/SharingBase.php +++ b/api/tests/Vfs/SharingBase.php @@ -77,14 +77,15 @@ class SharingBase extends LoggedInTest protected function tearDown() : void { - LoggedInTest::tearDownAfterClass(); + try + { + // Some tests may leave us logged out, which will cause failures in parent cleanup + LoggedInTest::tearDownAfterClass(); + } + catch(\Throwable $e) {} + LoggedInTest::setupBeforeClass(); - // Re-init, since they look at user, fstab, etc. - // Also, further tests that access the filesystem fail if we don't - Vfs::clearstatcache(); - Vfs::init_static(); - Vfs\StreamWrapper::init_static(); // Need to ask about mounts, or other tests fail Vfs::mount(); @@ -156,6 +157,10 @@ class SharingBase extends LoggedInTest { $dir .= '/'; } + if(!Vfs::is_readable($dir)) + { + Vfs::mkdir($dir); + } $this->files += $this->addFiles($dir); $logged_in_files = array_map( @@ -233,7 +238,7 @@ class SharingBase extends LoggedInTest switch($mode) { case Sharing::READONLY: - $this->assertFalse(Vfs::is_writable($file)); + $this->assertFalse(Vfs::is_writable($file), "Readonly share file '$file' is writable"); if(!Vfs::is_dir($file)) { // We expect this to fail @@ -254,6 +259,24 @@ class SharingBase extends LoggedInTest } + /** + * Mount the app entries into the filesystem + * + * @param string $path + */ + protected function mountLinks($path) + { + Vfs::$is_root = true; + $url = Links\StreamWrapper::PREFIX . '/apps'; + $this->assertTrue( + Vfs::mount($url, $path, false, false), + "Unable to mount $url => $path" + ); + Vfs::$is_root = false; + + $this->mounts[] = $path; + } + /** * Start versioning for the given path * @@ -501,7 +524,7 @@ class SharingBase extends LoggedInTest else { // If it's a file, check to make sure we get the file - $this->checkSharedFile($link, $mimetype); + $this->checkSharedFile($link, $mimetype, $share); } // Load share @@ -520,8 +543,7 @@ class SharingBase extends LoggedInTest $this->assertTrue(Vfs::is_readable('/'), 'Could not read root (/) from link'); // Check other paths - $this->assertFalse(Vfs::is_readable($path), "Was able to read $path as anoymous, it should be mounted as /"); - $this->assertFalse(Vfs::is_readable($path . '../')); + $this->assertFalse(Vfs::is_readable($path), "Was able to read $path as anonymous, it should be mounted as /"); } /** @@ -537,6 +559,13 @@ class SharingBase extends LoggedInTest $curl = curl_init($link); curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); + $cookie = ''; + if($GLOBALS['egw']->session->sessionid || $share['share_with']) + { + $session_id = $GLOBALS['egw']->session->sessionid ?: $share['share_with']; + $cookie .= ';'.Api\Session::EGW_SESSION_NAME."={$session_id}"; + } + curl_setopt($curl, CURLOPT_COOKIE, $cookie); $html = curl_exec($curl); curl_close($curl); @@ -568,7 +597,7 @@ class SharingBase extends LoggedInTest // Make sure we start at root, not somewhere else like the token mounted // as a sub-directory - $this->assertEquals('/', $data->data->content->nm->path); + $this->assertEquals('/', $data->data->content->nm->path, "Share was not mounted at /"); unset($data->data->content->nm->actions); //var_dump($data->data->content->nm); @@ -580,16 +609,17 @@ class SharingBase extends LoggedInTest * @param $link Share URL * @param $file Vfs path to file */ - public function checkSharedFile($link, $mimetype) + public function checkSharedFile($link, $mimetype, $share) { - stream_context_set_default( + $context = stream_context_create( array( 'http' => array( - 'method' => 'HEAD' + 'method' => 'HEAD', + 'header' => "Cookie: XDEBUG_SESSION=PHPSTORM;".Api\Session::EGW_SESSION_NAME.'=' . $share['share_with'] ) ) ); - $headers = get_headers($link); + $headers = get_headers($link, false, $context); $this->assertEquals('200', substr($headers[0], 9, 3), 'Did not find the file, got ' . $headers[0]); $indexed_headers = array(); diff --git a/api/tests/Vfs/StreamWrapperBase.php b/api/tests/Vfs/StreamWrapperBase.php index 2100a39958..24f4f661e7 100644 --- a/api/tests/Vfs/StreamWrapperBase.php +++ b/api/tests/Vfs/StreamWrapperBase.php @@ -21,7 +21,7 @@ use EGroupware\Api\Vfs; use EGroupware\Stylite\Vfs\Versioning; -class StreamWrapperBase extends LoggedInTest +abstract class StreamWrapperBase extends LoggedInTest { /** * How much should be logged to the console (stdout) @@ -32,6 +32,11 @@ class StreamWrapperBase extends LoggedInTest */ const LOG_LEVEL = 0; + /** + * @var string If we're just doing a simple test with one file, use this file + */ + protected $test_file = ''; + /** * Keep track of files to remove after * @var Array @@ -50,6 +55,24 @@ class StreamWrapperBase extends LoggedInTest 'maxdepth' => 5 ); + // User for testing - we share with this user & log in as them for checking + protected $account_id; + + // File that should not be available due to permissions + protected $no_access; + + // Use a completely new user, so we know it's there and "clean" + protected $account = array( + 'account_lid' => 'user_test', + 'account_firstname' => 'Access', + 'account_lastname' => 'Test', + 'account_passwd' => 'passw0rd', + 'account_passwd_2' => 'passw0rd', + // Don't let them in Default, any set ACLs will interfere with tests + 'account_primary_group' => 'Testers', + 'account_groups' => ['Testers'] + ); + protected function setUp() : void { // Check we have basic access @@ -61,19 +84,14 @@ class StreamWrapperBase extends LoggedInTest { $this->markTestSkipped('No write access to files dir "' .$GLOBALS['egw_info']['server']['files_dir'].'"' ); } + $this->mount(); } protected function tearDown() : void { - LoggedInTest::tearDownAfterClass(); - LoggedInTest::setupBeforeClass(); - - // Re-init, since they look at user, fstab, etc. - // Also, further tests that access the filesystem fail if we don't - Vfs::clearstatcache(); - Vfs::init_static(); - Vfs\StreamWrapper::init_static(); - + // Make sure we're on the original user. Failures could cause us to be logged in as someone else + $this->switchUser($GLOBALS['EGW_USER'], $GLOBALS['EGW_PASSWORD']); + $this->mount(); // Need to ask about mounts, or other tests fail Vfs::mount(); @@ -82,12 +100,19 @@ class StreamWrapperBase extends LoggedInTest if(static::LOG_LEVEL > 1) { + if($this->account_id) error_log($this->getName() . ' user to be removed: ' . $this->account_id); error_log($this->getName() . ' files for removal:'); error_log(implode("\n",$this->files)); error_log($this->getName() . ' mounts for removal:'); error_log(implode("\n",$this->mounts)); } + // Remove our other test user + if($this->account_id) + { + $GLOBALS['egw']->accounts->delete($this->account_id); + } + // Remove any added files (as root to limit versioning issues) if(in_array('/',$this->files)) { @@ -107,6 +132,248 @@ class StreamWrapperBase extends LoggedInTest Vfs::$is_root = $backup; } + ///// + /// These tests will be run by every extending class, with + /// the extending class's setUp(). They can be overridden, but + /// we get free tests this way with no copy/paste + ///// + + /** + * Simple test that we can write something and it's there + * By putting it in the base class, this test gets run for every backend + */ + public function testSimpleReadWrite() : string + { + if(!$this->test_file) + { + $this->markTestSkipped("No test file set - set it in setUp() or overriding test"); + } + + // Check that the file is not there + $pre_start = Vfs::stat($this->test_file); + $this->assertEquals(null,$pre_start, + "File '$this->test_file' was there before we started, check clean up" + ); + + // Write + $contents = $this->getName() . "\nJust a test ;)\n"; + $this->assertNotFalse( + file_put_contents(Vfs::PREFIX . $this->test_file, $contents), + "Could not write file $this->test_file" + ); + + // Check contents are unchanged + $this->assertEquals( + $contents, file_get_contents(Vfs::PREFIX . $this->test_file), + "Read file contents do not match what was written" + ); + + return $this->test_file; + } + + /** + * Simple delete of a file + * By putting it in the base class, this test gets run for every backend + * + * @depends testSimpleReadWrite + */ + public function testDelete($file) : void + { + if(!$this->test_file && !$file) + { + $this->markTestSkipped("No test file set - set it in setUp() or overriding test"); + } + + // Write + if(!$file) + { + $contents = $this->getName() . "\nJust a test ;)\n"; + $this->assertNotFalse( + file_put_contents(Vfs::PREFIX . $this->test_file, $contents), + "Could not write file $this->test_file" + ); + + $start = Vfs::stat($this->test_file); + $this->assertNotNull( + $start, + "File '$this->test_file' was not what we expected to find after writing" + ); + } + else + { + $this->test_file = $file; + } + + Vfs::unlink($this->test_file); + + $post = Vfs::stat($this->test_file); + $this->assertEquals(null,$post, + "File '$this->test_file' was there after deleting" + ); + } + + /** + * Check that a user with no permission to a file cannot access the file + * + * @depends testSimpleReadWrite + * @throws Api\Exception\AssertionFailed + */ + public function testNoReadAccess() : void + { + if(!$this->test_file) + { + $this->markTestSkipped("No test file set - set it in setUp() or overriding test"); + } + + // Check that the file is not there + $pre_start = Vfs::stat($this->test_file); + $this->assertEquals(null,$pre_start, + "File '$this->test_file' was there before we started, check clean up" + ); + + // Write + $file = $this->test_file; + $contents = $this->getName() . "\nJust a test ;)\n"; + $this->assertNotFalse( + file_put_contents(Vfs::PREFIX . $file, $contents), + "Could not write file $file" + ); + + // Create another user who has no access to our file + $user_b = $this->makeUser(); + + // Log in as them + $this->switchUser($this->account['account_lid'], $this->account['account_passwd']); + + $this->mount(); + + // Check the file + $this->assertFalse( + Vfs::is_readable($file), + "File '$file' was accessible by another user who had no permission" + ); + $this->assertFalse( + file_get_contents(Vfs::PREFIX . $file), + "Read someone else's file with no permission. " . Vfs::PREFIX . $file + ); + + } + + + /** + * Check that a user with permission to a file can access the file + * + * @depends testSimpleReadWrite + * @throws Api\Exception\AssertionFailed + */ + public function testWithAccess() : void + { + if(!$this->test_file) + { + $this->markTestSkipped("No test file set - set it in setUp() or overriding test"); + } + + // Check that the file is not there + $pre_start = Vfs::stat($this->test_file); + $this->assertEquals(null,$pre_start, + "File '$this->test_file' was there before we started, check clean up" + ); + + // Write + $file = $this->test_file; + $contents = $this->getName() . "\nJust a test ;)\n"; + $this->assertNotFalse( + file_put_contents(Vfs::PREFIX . $file, $contents), + "Could not write file $file" + ); + $pre = Vfs::stat($this->test_file); + + + // Create another user who has no access to our file + $user_b = $this->makeUser(); + + // Allow access + $this->allowAccess( + $this->getName(false), + $file, + $user_b, + 'r' + ); + + // Log in as them + $this->switchUser($this->account['account_lid'], $this->account['account_passwd']); + + $this->mount(); + + // Check the file + $post = Vfs::stat($file); + $this->assertNotNull($post, + "File '$file' was not accessible by another user who had permission" + ); + $this->assertEquals( + $contents, + file_get_contents(Vfs::PREFIX . $file), + "Problem reading contents of someone else's file (".Vfs::PREFIX . "$file) with permission" + ); + $this->assertTrue( + Vfs::is_readable($file), + "Vfs says $file is not readable. It should be." + ); + + } + + ////// Handy functions /////// + + /** + * Create a test user, returns the account ID + * + * @return int + */ + protected function makeUser(Array $account = []) : int + { + if(count($account) == 0) + { + $account = $this->account; + } + if(($account_id = $GLOBALS['egw']->accounts->name2id($account['account_lid']))) + { + // Delete if there in case something went wrong + $GLOBALS['egw']->accounts->delete($account_id); + } + + // It needs its own group too, Default will mess with any ACL tests + if(!$GLOBALS['egw']->accounts->exists($account['account_primary_group'])) + { + $group = $this->makeTestGroup(); + } + + // Execute + $command = new \admin_cmd_edit_user(false, $account); + $command->comment = 'Needed for unit test ' . $this->getName(); + $command->run(); + $this->account_id = $command->account; + + if($group) + { + // Had to create the group, but we don't want current user in it + $remove_group = new \admin_cmd_edit_group('Testers',['account_lid' => 'Testers', 'account_members' => [$this->account_id]]); + $remove_group->run(); + } + return $this->account_id; + } + + /** + * Make a test group we can put our users in to avoid any ACLs on Default group + */ + protected function makeTestGroup() + { + // Execute + $command = new \admin_cmd_edit_group(false, ['account_lid' => 'Testers', 'account_members' => $GLOBALS['egw_info']['user']['account_id']]); + $command->comment = 'Needed for unit test ' . $this->getName(); + $command->run(); + return $command->account; + } + /** * Make a filename that reflects the current test */ @@ -115,7 +382,48 @@ class StreamWrapperBase extends LoggedInTest if(is_null($path)) $path = Vfs::get_home_dir().'/'; if(substr($path,-1,1) !== '/') $path = $path . '/'; - return $path . get_class(this) . '_' . $this->getName() . '.txt'; + $reflect = new \ReflectionClass($this); + return $path . $reflect->getShortName() . '_' . $this->getName() . '.txt'; + } + + /** + * Mount the needed filesystem + * + * This may be called multiple times for each test as we change users, logout, etc. + */ + abstract protected function mount() : void; + + /** + * Allow access to the given file for the given user ID + * + * Using whatever way works best for the mount/streamwrapper being tested, allow the user access + * + * @param string $test_name + * @param string $test_file + * @param int $test_user + * @param string $needed r, w, rw + * @return mixed + */ + abstract protected function allowAccess(string $test_name, string &$test_file, int $test_user, string $needed) : void; + + /** + * Mount the app entries into the filesystem + * + * @param string $path + */ + protected function mountLinks($path) + { + Vfs::$is_root = true; + $url = Links\StreamWrapper::PREFIX . '/apps'; + $this->assertTrue( + Vfs::mount($url, $path, false, false), + "Unabe to mount $url => $path" + ); + Vfs::$is_root = false; + + $this->mounts[] = $path; + Vfs::clearstatcache(); + Vfs::init_static(); } /** @@ -187,7 +495,7 @@ class StreamWrapperBase extends LoggedInTest Vfs::$is_root = true; // I guess merge needs the dir in SQLFS first - if(!Vfs::is_dir($dir)) Vfs::mkdir($path); + if(!Vfs::is_dir($path)) Vfs::mkdir($path); Vfs::chmod($path, 0750); Vfs::chown($path, $GLOBALS['egw_info']['user']['account_id']); @@ -265,20 +573,4 @@ class StreamWrapperBase extends LoggedInTest */ return $files; } - - /** - * Make an infolog entry - */ - protected function make_infolog() - { - $bo = new \infolog_bo(); - $element = array( - 'info_subject' => "Test infolog for #{$this->getName()}", - 'info_des' => 'Test element for ' . $this->getName() . "\n" . Api\DateTime::to(), - 'info_status' => 'open' - ); - - $element_id = $bo->write($element, true, true, true, true); - return $element_id; - } } \ No newline at end of file diff --git a/api/tests/Vfs/StreamWrapperTest.php b/api/tests/Vfs/StreamWrapperTest.php index 1feb73bd1e..60944b66d7 100644 --- a/api/tests/Vfs/StreamWrapperTest.php +++ b/api/tests/Vfs/StreamWrapperTest.php @@ -24,7 +24,7 @@ class StreamWrapperTest extends StreamWrapperBase protected function setUp() : void { parent::setUp(); - + $this->files[] = $this->test_file = $this->getFilename(); } protected function tearDown() : void @@ -34,56 +34,27 @@ class StreamWrapperTest extends StreamWrapperBase parent::tearDown(); } - /** - * Simple test that we can write something and it's there - */ - public function testSimpleReadWrite() : void + public function testWithAccess() : void { - $this->files[] = $test_file = $this->getFilename(); - $contents = $this->getName() . "\nJust a test ;)\n"; - $this->assertNotFalse( - file_put_contents(Vfs::PREFIX . $test_file, $contents), - "Could not write file $test_file" - ); + // Put it in the group directory this time so we can give access + $this->files[] = $this->test_file = $this->getFilename('/home/Default'); - // Check contents are unchanged - $this->assertEquals( - $contents, file_get_contents(Vfs::PREFIX . $test_file), - "Read file contents do not match what was written" - ); + parent::testWithAccess(); } - /** - * Simple delete of a file - */ - public function testDelete() : void + protected function mount(): void { - $this->files[] = $test_file = $this->getFilename(); + // Nothing here + } - // Check that the file is not there - $pre_start = Vfs::stat($test_file); - $this->assertEquals(null,$pre_start, - "File '$test_file' was there before we started, check clean up" - ); + protected function allowAccess(string $test_name, string &$test_file, int $test_user, string $needed) : void + { + // We'll allow access by putting test user in Default group + $command = new \admin_cmd_edit_user($test_user, ['account_groups' => array_merge($this->account['account_groups'],['Default'])]); + $command->run(); - // Write - $contents = $this->getName() . "\nJust a test ;)\n"; - $this->assertNotFalse( - file_put_contents(Vfs::PREFIX . $test_file, $contents), - "Could not write file $test_file" - ); + // Add explicit permission on group + Vfs::chmod($test_file, Vfs::mode2int('g+'.$needed)); - $start = Vfs::stat($test_file); - $this->assertNotNull( - $start, - "File '$test_file' was not what we expected to find after writing" - ); - - Vfs::unlink($test_file); - - $post = Vfs::stat($test_file); - $this->assertEquals(null,$post, - "File '$test_file' was there after deleting" - ); } } \ No newline at end of file diff --git a/composer.json b/composer.json index 5860327908..980e36d300 100644 --- a/composer.json +++ b/composer.json @@ -49,12 +49,12 @@ ], "config": { "platform": { - "php": "7.2" + "php": "7.3" }, "sort-packages": true }, "require": { - "php": ">=7.2,<=8.0.0alpha1", + "php": ">=7.3,<=8.0.0alpha1", "ext-gd": "*", "ext-json": "*", "ext-mysqli": "*", @@ -87,6 +87,8 @@ "egroupware/tracker": "self.version", "egroupware/z-push-dev": "^2.5", "fxp/composer-asset-plugin": "^1.2.2", + "egroupware/guzzlestream": "dev-master", + "egroupware/webdav": "dev-master", "npm-asset/as-jqplot": "1.0.*", "npm-asset/gridster": "0.5.*", "oomphinc/composer-installers-extender": "^1.1", diff --git a/composer.lock b/composer.lock index 4bf7e4366b..c2760991e5 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "bc3a7b62de6792d67aaf9f127480abea", + "content-hash": "6a700a309b5c1bc30c563ccb2526e7e2", "packages": [ { "name": "adldap2/adldap2", @@ -932,6 +932,62 @@ "homepage": "https://www.egroupware.org/", "time": "2020-08-19T13:40:53+00:00" }, + { + "name": "egroupware/guzzlestream", + "version": "dev-master", + "target-dir": "Guzzle/Stream", + "source": { + "type": "git", + "url": "https://github.com/EGroupware/stream.git", + "reference": "d29fc35ebf3bd752308520aa5f17a3e5500f6af3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/EGroupware/stream/zipball/d29fc35ebf3bd752308520aa5f17a3e5500f6af3", + "reference": "d29fc35ebf3bd752308520aa5f17a3e5500f6af3", + "shasum": "" + }, + "require": { + "guzzle/common": "^3.9.2", + "php": ">=5.3.2" + }, + "replace": { + "guzzle/stream": "*" + }, + "suggest": { + "guzzle/http": "To convert Guzzle request objects to PHP streams" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.7-dev" + } + }, + "autoload": { + "psr-0": { + "Guzzle\\Stream": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle stream wrapper component", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "Guzzle", + "component", + "stream" + ], + "time": "2020-09-23T16:33:47+00:00" + }, { "name": "egroupware/icalendar", "version": "2.1.9", @@ -976,12 +1032,12 @@ { "name": "Chuck Hagenbuch", "email": "chuck@horde.org", - "role": "Lead" + "role": "lead" }, { "name": "Jan Schneider", "email": "jan@horde.org", - "role": "Lead" + "role": "lead" }, { "name": "Michael J Rubinsky", @@ -1028,7 +1084,7 @@ ], "description": "Compiled version of magicsuggest customized for EGroupware project.", "homepage": "https://github.com/EGroupware/magicsuggest", - "time": "2018-06-21T13:36:37+00:00" + "time": "2018-06-21T10:14:03+00:00" }, { "name": "egroupware/news_admin", @@ -1409,6 +1465,58 @@ "homepage": "https://www.egroupware.org/", "time": "2020-08-28T17:39:07+00:00" }, + { + "name": "egroupware/webdav", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/EGroupware/WebDAV.git", + "reference": "889da78b6489965df8a379ccdc25853fe74da199" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/EGroupware/WebDAV/zipball/889da78b6489965df8a379ccdc25853fe74da199", + "reference": "889da78b6489965df8a379ccdc25853fe74da199", + "shasum": "" + }, + "require": { + "guzzle/http": "~3.0", + "php": ">=5.3.0", + "psr/log": "~1.0" + }, + "require-dev": { + "phpunit/phpunit": "~3.7.0" + }, + "suggest": { + "monolog/monolog": "Adds support for logging HTTP requests and responses", + "symfony/finder": "Allows you to more easily filter the files that the stream wrapper returns" + }, + "type": "library", + "autoload": { + "psr-0": { + "Grale\\WebDav": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Geoffroy Letournel", + "email": "geoffroy.letournel@gmail.com" + } + ], + "description": "A simple PHP WebDAV client and stream wrapper", + "homepage": "https://github.com/gletournel/WebDAV", + "keywords": [ + "WebDAV", + "php", + "stream", + "wrapper" + ], + "time": "2020-09-23T16:16:07+00:00" + }, { "name": "egroupware/z-push-dev", "version": "2.5.0", @@ -1668,6 +1776,154 @@ ], "time": "2019-11-13T10:30:21+00:00" }, + { + "name": "guzzle/common", + "version": "v3.9.2", + "target-dir": "Guzzle/Common", + "source": { + "type": "git", + "url": "https://github.com/Guzzle3/common.git", + "reference": "2e36af7cf2ce3ea1f2d7c2831843b883a8e7b7dc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Guzzle3/common/zipball/2e36af7cf2ce3ea1f2d7c2831843b883a8e7b7dc", + "reference": "2e36af7cf2ce3ea1f2d7c2831843b883a8e7b7dc", + "shasum": "" + }, + "require": { + "php": ">=5.3.2", + "symfony/event-dispatcher": ">=2.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.7-dev" + } + }, + "autoload": { + "psr-0": { + "Guzzle\\Common": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Common libraries used by Guzzle", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "collection", + "common", + "event", + "exception" + ], + "abandoned": "guzzle/guzzle", + "time": "2014-08-11T04:32:36+00:00" + }, + { + "name": "guzzle/http", + "version": "v3.9.2", + "target-dir": "Guzzle/Http", + "source": { + "type": "git", + "url": "https://github.com/Guzzle3/http.git", + "reference": "1e8dd1e2ba9dc42332396f39fbfab950b2301dc5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Guzzle3/http/zipball/1e8dd1e2ba9dc42332396f39fbfab950b2301dc5", + "reference": "1e8dd1e2ba9dc42332396f39fbfab950b2301dc5", + "shasum": "" + }, + "require": { + "guzzle/common": "self.version", + "guzzle/parser": "self.version", + "guzzle/stream": "self.version", + "php": ">=5.3.2" + }, + "suggest": { + "ext-curl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.7-dev" + } + }, + "autoload": { + "psr-0": { + "Guzzle\\Http": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "HTTP libraries used by Guzzle", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "Guzzle", + "client", + "curl", + "http", + "http client" + ], + "abandoned": "guzzle/guzzle", + "time": "2014-08-11T04:32:36+00:00" + }, + { + "name": "guzzle/parser", + "version": "v3.9.2", + "target-dir": "Guzzle/Parser", + "source": { + "type": "git", + "url": "https://github.com/Guzzle3/parser.git", + "reference": "6874d171318a8e93eb6d224cf85e4678490b625c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Guzzle3/parser/zipball/6874d171318a8e93eb6d224cf85e4678490b625c", + "reference": "6874d171318a8e93eb6d224cf85e4678490b625c", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.7-dev" + } + }, + "autoload": { + "psr-0": { + "Guzzle\\Parser": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Interchangeable parsers used by Guzzle", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "URI Template", + "cookie", + "http", + "message", + "url" + ], + "abandoned": "guzzle/guzzle", + "time": "2014-02-05T18:29:46+00:00" + }, { "name": "imsglobal/lti-1p3-tool", "version": "dev-master", @@ -2345,7 +2601,7 @@ "license": [ "LGPL-2.1" ], - "description": "A library to wrap various compression techniques." + "description": "An API for various compression techniques." }, { "name": "pear-pear.horde.org/Horde_Crypt", @@ -2411,7 +2667,7 @@ "license": [ "LGPL-2.1" ], - "description": "A library that provides blowfish encryption/decryption for PHP string data." + "description": "Provides blowfish encryption/decryption for PHP string data." }, { "name": "pear-pear.horde.org/Horde_Date", @@ -2527,7 +2783,7 @@ "license": [ "BSD-2-Clause" ], - "description": "A library that wraps various backends providing IDNA (Internationalized Domain Names in Applications) support." + "description": "Normalized access to various backends providing IDNA (Internationalized Domain Names in Applications) support." }, { "name": "pear-pear.horde.org/Horde_Imap_Client", @@ -2565,7 +2821,7 @@ "license": [ "LGPL-2.1" ], - "description": "A library to access IMAP4rev1 (RFC 3501) mail servers. Also supports connections to POP3 (STD 53/RFC 1939)." + "description": "Interface to access IMAP4rev1 (RFC 3501) mail servers. Also supports connections to POP3 (STD 53/RFC 1939)." }, { "name": "pear-pear.horde.org/Horde_ListHeaders", @@ -2845,7 +3101,7 @@ "license": [ "LGPL-2.1" ], - "description": "A library that provides an abstract PHP network socket client." + "description": "Provides abstract class for use in creating PHP network socket clients." }, { "name": "pear-pear.horde.org/Horde_Stream", @@ -2986,7 +3242,7 @@ "license": [ "LGPL-2.1" ], - "description": "A library that provides a text-based diff engine and renderers for multiple diff output formats." + "description": "A text-based diff engine and renderers for multiple diff output formats." }, { "name": "pear-pear.horde.org/Horde_Text_Flowed", @@ -3014,7 +3270,7 @@ "license": [ "LGPL-2.1" ], - "description": "A library that provides common methods for manipulating text using the encoding described in RFC 3676 ('flowed' text)." + "description": "The Horde_Text_Flowed:: class provides common methods for manipulating text using the encoding described in RFC 3676 ('flowed' text)." }, { "name": "pear-pear.horde.org/Horde_Translation", @@ -3097,7 +3353,7 @@ "license": [ "LGPL-2.1" ], - "description": "A library that provides functionality useful for all kind of applications." + "description": "These classes provide functionality useful for all kind of applications." }, { "name": "pear/archive_tar", @@ -3784,12 +4040,6 @@ } ], "description": "PHPMailer is a full-featured email creation and transfer class for PHP", - "funding": [ - { - "url": "https://github.com/synchro", - "type": "github" - } - ], "time": "2020-05-27T12:24:03+00:00" }, { @@ -3882,20 +4132,6 @@ "x.509", "x509" ], - "funding": [ - { - "url": "https://github.com/terrafrost", - "type": "github" - }, - { - "url": "https://www.patreon.com/phpseclib", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/phpseclib/phpseclib", - "type": "tidelift" - } - ], "time": "2020-04-04T23:17:33+00:00" }, { @@ -6110,20 +6346,6 @@ ], "description": "Symfony Config Component", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-03-27T16:54:36+00:00" }, { @@ -6181,20 +6403,6 @@ ], "description": "Symfony Debug Component", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-08-10T07:47:39+00:00" }, { @@ -6268,20 +6476,6 @@ ], "description": "Symfony DependencyInjection Component", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-03-30T10:09:30+00:00" }, { @@ -6339,20 +6533,6 @@ ], "description": "Symfony ErrorHandler Component", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-08-17T09:56:45+00:00" }, { @@ -6423,20 +6603,6 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-08-13T14:18:44+00:00" }, { @@ -6499,20 +6665,6 @@ "interoperability", "standards" ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-07-06T13:19:58+00:00" }, { @@ -6563,20 +6715,6 @@ ], "description": "Symfony Filesystem Component", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-03-27T16:54:36+00:00" }, { @@ -6632,20 +6770,6 @@ ], "description": "Symfony HttpFoundation Component", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-08-17T07:39:58+00:00" }, { @@ -6737,20 +6861,6 @@ ], "description": "Symfony HttpKernel Component", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-09-02T08:09:29+00:00" }, { @@ -6813,20 +6923,6 @@ "mime", "mime-type" ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-08-17T09:56:45+00:00" }, { @@ -6889,20 +6985,6 @@ "polyfill", "portable" ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-07-14T12:35:20+00:00" }, { @@ -6974,20 +7056,6 @@ "portable", "shim" ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-08-04T06:02:08+00:00" }, { @@ -7055,20 +7123,6 @@ "portable", "shim" ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-07-14T12:35:20+00:00" }, { @@ -7132,20 +7186,6 @@ "portable", "shim" ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-07-14T12:35:20+00:00" }, { @@ -7265,20 +7305,6 @@ "portable", "shim" ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-07-14T12:35:20+00:00" }, { @@ -7338,20 +7364,6 @@ "portable", "shim" ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-07-14T12:35:20+00:00" }, { @@ -7414,20 +7426,6 @@ "portable", "shim" ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-07-14T12:35:20+00:00" }, { @@ -7494,20 +7492,6 @@ "portable", "shim" ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-07-14T12:35:20+00:00" }, { @@ -7636,20 +7620,6 @@ "uri", "url" ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-03-30T11:41:10+00:00" }, { @@ -7785,20 +7755,6 @@ "debug", "dump" ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-08-17T07:31:35+00:00" }, { @@ -7858,20 +7814,6 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], "time": "2020-03-30T11:41:10+00:00" }, { @@ -8257,6 +8199,58 @@ ], "time": "2019-10-21T16:45:58+00:00" }, + { + "name": "grale/webdav", + "version": "v0.2.1", + "source": { + "type": "git", + "url": "https://github.com/gletournel/WebDAV.git", + "reference": "c4d592e90f68806e491544d780fb44c78e3961cb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/gletournel/WebDAV/zipball/c4d592e90f68806e491544d780fb44c78e3961cb", + "reference": "c4d592e90f68806e491544d780fb44c78e3961cb", + "shasum": "" + }, + "require": { + "guzzle/http": "~3.0", + "php": ">=5.3.0", + "psr/log": "~1.0" + }, + "require-dev": { + "phpunit/phpunit": "~3.7.0" + }, + "suggest": { + "monolog/monolog": "Adds support for logging HTTP requests and responses", + "symfony/finder": "Allows you to more easily filter the files that the stream wrapper returns" + }, + "type": "library", + "autoload": { + "psr-0": { + "Grale\\WebDav": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Geoffroy Letournel", + "email": "geoffroy.letournel@gmail.com" + } + ], + "description": "A simple PHP WebDAV client and stream wrapper", + "homepage": "https://github.com/gletournel/WebDAV", + "keywords": [ + "WebDAV", + "php", + "stream", + "wrapper" + ], + "time": "2017-09-26T13:31:13+00:00" + }, { "name": "guzzlehttp/guzzle", "version": "6.5.2", @@ -9857,12 +9851,14 @@ "egroupware/smallpart": 20, "egroupware/status": 20, "egroupware/swoolepush": 20, - "egroupware/tracker": 20 + "egroupware/tracker": 20, + "egroupware/guzzlestream": 20, + "egroupware/webdav": 20 }, "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": ">=7.2,<=8.0.0alpha1", + "php": ">=7.3,<=8.0.0alpha1", "ext-gd": "*", "ext-json": "*", "ext-mysqli": "*", @@ -9874,7 +9870,6 @@ }, "platform-dev": [], "platform-overrides": { - "php": "7.2" - }, - "plugin-api-version": "1.1.0" + "php": "7.3" + } } diff --git a/filemanager/inc/class.filemanager_hooks.inc.php b/filemanager/inc/class.filemanager_hooks.inc.php index c90a70be51..93970c3b7f 100644 --- a/filemanager/inc/class.filemanager_hooks.inc.php +++ b/filemanager/inc/class.filemanager_hooks.inc.php @@ -306,7 +306,7 @@ class filemanager_hooks { foreach (Api\Hooks::process('filemanager-editor-link', 'collabora') as $app => $link) { - if ($link && ($access = \EGroupware\Api\Vfs\Links\StreamWrapper::check_app_rights($app)) && + if ($link && !empty($GLOBALS['egw_info']['user']['apps'][$app]) && (empty($GLOBALS['egw_info']['user']['preferences']['filemanager']['document_doubleclick_action']) || $GLOBALS['egw_info']['user']['preferences']['filemanager']['document_doubleclick_action'] == $app)) { diff --git a/infolog/inc/class.infolog_bo.inc.php b/infolog/inc/class.infolog_bo.inc.php index 575c6047ab..496fb13da1 100644 --- a/infolog/inc/class.infolog_bo.inc.php +++ b/infolog/inc/class.infolog_bo.inc.php @@ -46,6 +46,12 @@ class infolog_bo * @var boolean */ var $log = false; + + /** + * Access permission cache for current user + */ + protected static $access_cache = array(); + /** * Cached timezone data * @@ -330,15 +336,13 @@ class infolog_bo */ function check_access($info,$required_rights,$other=0,$user=null) { - static $cache = array(); - $info_id = is_array($info) ? $info['info_id'] : $info; if (!$user) $user = $this->user; if ($user == $this->user) { $grants = $this->grants; - if ($info_id) $access =& $cache[$info_id][$required_rights]; // we only cache the current user! + if ($info_id) $access =& static::$access_cache[$info_id][$required_rights]; // we only cache the current user! } else { @@ -410,6 +414,7 @@ class infolog_bo */ function init() { + static::$access_cache = array(); $this->so->init(); } diff --git a/vfs-context-links.php b/vfs-context-links.php new file mode 100644 index 0000000000..9de82bad60 --- /dev/null +++ b/vfs-context-links.php @@ -0,0 +1,68 @@ + [ + 'currentapp' => 'login', + ], +]; +require_once __DIR__.'/header-default.inc.php'; +$_SESSION = []; // reset session, specially cache for links +$egw->session->create($sysop='ralf', '', '', true, false); + +$so = new infolog_so(); +$so->delete(['info_id' => [1, 2]]); +$infolog_sysop = $so->write(['info_id' => 1, 'info_owner' => 5, 'info_subject' => 'Test-InfoLog Ralf', 'info_type' => 'task'], 0, null, true); +// anonymous user give not grants, has not shared groups with Ralf or sysop +$infolog_anon = $so->write(['info_id' => 2, 'info_owner' => ($anon=Api\Accounts::getInstance()->name2id('anonymous')), 'info_subject' => 'Test-InfoLog Anonymous', 'info_type' => 'task'], 0, null, true); +//var_dump($so->read(['info_id' => $infolog_sysop]), $so->read(['info_id' => $infolog_anon])); +// anonymous user needs infolog run rights for further tests +$acl = new Api\Acl($anon); +$acl->add_repository('infolog', 'run', $anon, 1); + +$schema = 'stylite.links'; //'links'; +Vfs::$is_root = true; +Vfs::mount("$schema://default/apps", '/apps', false, false); +Vfs::$is_root = false; +var_dump(Vfs::mount()); + +$infolog_sysop_dir = "/apps/infolog/$infolog_sysop"; +$infolog_anon_dir = "/apps/infolog/$infolog_anon"; +var_dump(file_put_contents("vfs://default$infolog_sysop_dir/test.txt", "Just a test ;)\n")); +var_dump("Vfs::proppatch('$infolog_sysop_dir/test.txt', [['ns' => Vfs::DEFAULT_PROP_NAMESPACE, 'name' => 'test', 'val' => 'something']])=" . array2string(Vfs::proppatch("$infolog_sysop_dir/test.txt", [['ns' => Vfs::DEFAULT_PROP_NAMESPACE, 'name' => 'test', 'val' => 'something']])), + "Vfs::propfind('$infolog_sysop_dir/test.txt')=" . json_encode(Vfs::propfind("$infolog_sysop_dir/test.txt"), JSON_UNESCAPED_SLASHES)); + +var_dump($f = fopen("vfs://default$infolog_sysop_dir/test.txt", 'r'), fread($f, 100), fclose($f)); + +Vfs::$is_root = true; +var_dump(file_put_contents("vfs://default$infolog_anon_dir/test.txt", "Just a test ;)\n")); +var_dump("Vfs::proppatch('$infolog_anon_dir/test.txt', [['ns' => Vfs::DEFAULT_PROP_NAMESPACE, 'name' => 'test', 'val' => 'something']])=" . array2string(Vfs::proppatch("$infolog_anon_dir/test.txt", [['ns' => Vfs::DEFAULT_PROP_NAMESPACE, 'name' => 'test', 'val' => 'something']])), + "Vfs::propfind('$infolog_anon_dir/test.txt')=" . json_encode(Vfs::propfind("$infolog_anon_dir/test.txt"), JSON_UNESCAPED_SLASHES)); +var_dump(Vfs::mount(/*"$schema://anonymous@default$infolog_anon_dir"*/"vfs://anonymous@default$infolog_anon_dir", $share_dir = "/home/$sysop/anon-infolog", false, false)); +Vfs::$is_root = false; + +var_dump(Vfs::mount()); + +var_dump("Vfs::resolve_url('$share_dir/test.txt')=" . Vfs::resolve_url("$share_dir/test.txt")); +var_dump("Vfs::url_stat('$share_dir/test.txt')=" . json_encode(Vfs::stat("$share_dir/test.txt"), JSON_UNESCAPED_SLASHES)); +var_dump("Vfs::is_readable('$share_dir/test.txt')=" . json_encode(Vfs::is_readable("$share_dir/test.txt"))); +var_dump("fopen('vfs://default$share_dir/test.txt', 'r')", $f = fopen("vfs://default$share_dir/test.txt", 'r'), fread($f, 100), fclose($f)); +var_dump("Vfs::propfind('$share_dir/test.txt')", Vfs::propfind("$share_dir/test.txt")); +var_dump("Vfs::proppatch('$share_dir/test.txt', [['ns' => Vfs::DEFAULT_PROP_NAMESPACE, 'name' => 'test', 'val' => 'something else']])=" . array2string(Vfs::proppatch("$share_dir/test.txt", [['ns' => Vfs::DEFAULT_PROP_NAMESPACE, 'name' => 'test', 'val' => 'something else']])), + "Vfs::propfind('$share_dir/test.txt')=" . json_encode(Vfs::propfind("$share_dir/test.txt"), JSON_UNESCAPED_SLASHES)); + +var_dump("Vfs::url_stat('$share_dir/test-dir')=" . json_encode(Vfs::stat("$share_dir/test-dir"))); +var_dump("Vfs::mkdir('$share_dir/test-dir')=" . json_encode(Vfs::mkdir("$share_dir/test-dir"))); +var_dump("Vfs::url_stat('$share_dir/test-dir')=" . json_encode(Vfs::stat("$share_dir/test-dir"), JSON_UNESCAPED_SLASHES)); +var_dump(file_put_contents("vfs://default$share_dir/test-dir/test.txt", "Just a test ;)\n")); +var_dump("Vfs::url_stat('$share_dir/test-dir/test.txt')=" . json_encode(Vfs::stat("$share_dir/test-dir"), JSON_UNESCAPED_SLASHES)); +var_dump(file_get_contents("vfs://default$share_dir/test-dir/test.txt")); +var_dump("Vfs::unlink('$share_dir/test-dir/test.txt')=" . json_encode(Vfs::unlink("$share_dir/test-dir/test.txt"))); +var_dump("Vfs::rmdir('$share_dir/test-dir')=" . json_encode(Vfs::rmdir("$share_dir/test-dir"))); +var_dump("Vfs::url_stat('$share_dir/test-dir')=" . json_encode(Vfs::stat("$share_dir/test-dir"))); + +var_dump("Vfs::scandir('$share_dir')=" . json_encode(Vfs::scandir($share_dir), JSON_UNESCAPED_SLASHES)); +var_dump("Vfs::remove('$share_dir/test.txt')=" . json_encode(Vfs::remove("$share_dir/test.txt"), JSON_UNESCAPED_SLASHES)); +var_dump("Vfs::scandir('$share_dir')=" . json_encode(Vfs::scandir($share_dir), JSON_UNESCAPED_SLASHES)); diff --git a/vfs-context-share.php b/vfs-context-share.php new file mode 100644 index 0000000000..46f6f504d5 --- /dev/null +++ b/vfs-context-share.php @@ -0,0 +1,72 @@ + [ + 'currentapp' => 'login', + ], +]; +require_once __DIR__.'/header-default.inc.php'; + +$GLOBALS['egw_info']['user'] = [ + 'account_id' => 5, + 'account_lid' => $sysop='ralf', +]; + +$other = 'birgit'; + +$schema = 'sqlfs';//'stylite.versioning'; //'sqlfs'; +Vfs::$is_root = true; +Vfs::mount("$schema://default/home", '/home', false, false); +Vfs::$is_root = false; +var_dump(Vfs::mount()); +//var_dump(Vfs::scandir('/home')); +//var_dump(Vfs::find('/home', ['maxdepth' => 1])); +//var_dump(Vfs::scandir("/home/$sysop")); + +var_dump(file_put_contents("vfs://default/home/$sysop/test.txt", "Just a test ;)\n")); +var_dump("Vfs::proppatch('/home/$sysop/test.txt', [['ns' => Vfs::DEFAULT_PROP_NAMESPACE, 'name' => 'test', 'val' => 'something']])=".array2string(Vfs::proppatch("/home/$sysop/test.txt", [['ns' => Vfs::DEFAULT_PROP_NAMESPACE, 'name' => 'test', 'val' => 'something']])), + "Vfs::propfind('/home/$sysop/test.txt')=".json_encode(Vfs::propfind("/home/$sysop/test.txt"), JSON_UNESCAPED_SLASHES)); + +var_dump($f=fopen("vfs://default/home/$sysop/test.txt", 'r'), fread($f, 100), fclose($f)); +//var_dump(Vfs::find("/home/$sysop", ['maxdepth' => 1])); + +Vfs::$is_root = true; +var_dump(file_put_contents("vfs://default/home/$other/test.txt", "Just a test ;)\n")); +var_dump("Vfs::proppatch('/home/$other/test.txt', [['ns' => Vfs::DEFAULT_PROP_NAMESPACE, 'name' => 'test', 'val' => 'something']])=".array2string(Vfs::proppatch("/home/$other/test.txt", [['ns' => Vfs::DEFAULT_PROP_NAMESPACE, 'name' => 'test', 'val' => 'something']])), + "Vfs::propfind('/home/$other/test.txt')=".json_encode(Vfs::propfind("/home/$other/test.txt"), JSON_UNESCAPED_SLASHES)); +$backup = Vfs::$user; Vfs::$user = Api\Accounts::getInstance()->name2id($other); +$share = stylite_sharing::create("/home/$other", stylite_sharing::WRITABLE, '', $sysop); +Vfs::$user = $backup; +var_dump($share); +var_dump(Vfs::mount($sharing_url="sharing://$share[share_token]@default/", "/home/$sysop/$other", false, false)); +Vfs::$is_root = false; + +var_dump(Vfs::mount()); + +var_dump(Vfs::load_wrapper('sharing')); +var_dump(stat($sharing_url.'test.txt')); +var_dump(file_get_contents($sharing_url.'test.txt')); + +var_dump("Vfs::resolve_url('/home/$sysop/$other/test.txt')=".Vfs::resolve_url("/home/$sysop/$other/test.txt")); +var_dump("Vfs::stat('/home/$sysop/$other/test.txt')=".json_encode(Vfs::stat("/home/$sysop/$other/test.txt"), JSON_UNESCAPED_SLASHES)); +var_dump("Vfs::is_readable('/home/$sysop/$other/test.txt')=".json_encode(Vfs::is_readable("/home/$sysop/$other/test.txt"))); +var_dump("fopen('vfs://default/home/$sysop/$other/test.txt', 'r')", $f=fopen("vfs://default/home/$sysop/$other/test.txt", 'r'), fread($f, 100), fclose($f)); + +var_dump("Vfs::propfind('/home/$sysop/$other/test.txt')", Vfs::propfind("/home/$sysop/$other/test.txt")); +var_dump("Vfs::proppatch('/home/$sysop/$other/test.txt', [['ns' => Vfs::DEFAULT_PROP_NAMESPACE, 'name' => 'test', 'val' => 'something else']])=".array2string(Vfs::proppatch("/home/$sysop/$other/test.txt", [['ns' => Vfs::DEFAULT_PROP_NAMESPACE, 'name' => 'test', 'val' => 'something else']])), + "Vfs::propfind('/home/$sysop/$other/test.txt')=".json_encode(Vfs::propfind("/home/$sysop/$other/test.txt"), JSON_UNESCAPED_SLASHES)); + +var_dump("Vfs::url_stat('/home/$sysop/$other/test-dir')=".json_encode(Vfs::stat("/home/$sysop/$other/test-dir"))); +var_dump("Vfs::mkdir('/home/$sysop/$other/test-dir')=".json_encode(Vfs::mkdir("/home/$sysop/$other/test-dir"))); +var_dump("Vfs::scandir('/home/$sysop/$other')=".json_encode(Vfs::scandir("/home/$sysop/$other"), JSON_UNESCAPED_SLASHES)); +var_dump("Vfs::stat('/home/$sysop/$other/test-dir')=".json_encode(Vfs::stat("/home/$sysop/$other/test-dir"), JSON_UNESCAPED_SLASHES)); +var_dump("Vfs::rmdir('/home/$sysop/$other/test-dir')=".json_encode(Vfs::rmdir("/home/$sysop/$other/test-dir"))); +var_dump("Vfs::stat('/home/$sysop/$other/test-dir')=".json_encode(Vfs::stat("/home/$sysop/$other/test-dir"))); +var_dump("Vfs::remove('/home/$sysop/$other/test.txt')=".json_encode(Vfs::remove("/home/$sysop/$other/test.txt"), JSON_UNESCAPED_SLASHES)); +var_dump("Vfs::stat('/home/$sysop/$other/test.txt')=".json_encode(Vfs::stat("/home/$sysop/$other/test.txt"), JSON_UNESCAPED_SLASHES)); +var_dump("Vfs::scandir('/home/$sysop/$other')=".json_encode(Vfs::scandir("/home/$sysop/$other"), JSON_UNESCAPED_SLASHES)); + +stylite_sharing::delete($share['share_id']); \ No newline at end of file diff --git a/vfs-context-webdav.php b/vfs-context-webdav.php new file mode 100644 index 0000000000..ecdec9a7e7 --- /dev/null +++ b/vfs-context-webdav.php @@ -0,0 +1,40 @@ + [ + 'currentapp' => 'login', + ], +]; +require_once __DIR__.'/header-default.inc.php'; + +$GLOBALS['egw_info']['user'] = [ + 'account_id' => 5, + 'account_lid' => $sysop='ralf', +]; + +$other = 'birgit'; + +WebDav\StreamWrapper::register(); + +var_dump(file_put_contents("vfs://default/home/$sysop/test.txt", "Just a test ;)\n")); + +$base = "webdavs://$sysop:secret@boulder.egroupware.org/egroupware/webdav.php"; +var_dump(scandir("$base/home")); + +Vfs::$is_root = true; +Vfs::mount("$base/home/$sysop", "/home/$sysop/webdav", false, false); +Vfs::$is_root = false; +var_dump(Vfs::mount()); +var_dump(Vfs::scandir("/home/$sysop/webdav")); +var_dump(file_get_contents("vfs://default/home/$sysop/webdav/test.txt")); +var_dump(Vfs::find("/home/$sysop/webdav", ['maxdepth' => 1], true)); +//var_dump(Vfs::scandir("/home/$sysop")); + +var_dump(scandir($share = "webdavs://pole.egroupware.org/egroupware/share.php/c2nqd6plwiTT8ha6U22sZXsLc7vkVdM3")); +Vfs::$is_root = true; +Vfs::mount("$share", "/home/$sysop/shares/PressRelease-20.1", false, false); +Vfs::$is_root = false; +var_dump(Vfs::find("/home/$sysop/shares/PressRelease-20.1", ['maxdepth' => 1], true)); diff --git a/vfs-context.php b/vfs-context.php new file mode 100644 index 0000000000..7cc52e8a76 --- /dev/null +++ b/vfs-context.php @@ -0,0 +1,66 @@ + [ + 'currentapp' => 'login', + ], +]; +require_once __DIR__.'/header-default.inc.php'; + +$GLOBALS['egw_info']['user'] = [ + 'account_id' => 5, + 'account_lid' => $sysop='ralf', +]; + +$other = 'birgit'; + +$schema = 'stylite.versioning'; //'sqlfs'; +Vfs::$is_root = true; +Vfs::mount("$schema://default/home", '/home', false, false); +Vfs::$is_root = false; +var_dump(Vfs::mount()); +var_dump(Vfs::scandir('/home')); +var_dump(Vfs::find('/home', ['maxdepth' => 1])); +//var_dump(Vfs::scandir("/home/$sysop")); + +Vfs::$is_root = true; +Vfs::mount('filesystem://default/var/lib/egroupware', "/home/$other/something", false, false); +Vfs::$is_root = false; +var_dump(Vfs::stat("/home/$other/something")); + +var_dump(file_put_contents("vfs://default/home/$sysop/test.txt", "Just a test ;)\n")); +var_dump("Vfs::proppatch('/home/$sysop/test.txt', [['ns' => Vfs::DEFAULT_PROP_NAMESPACE, 'name' => 'test', 'val' => 'something']])=".array2string(Vfs::proppatch("/home/$sysop/test.txt", [['ns' => Vfs::DEFAULT_PROP_NAMESPACE, 'name' => 'test', 'val' => 'something']])), + "Vfs::propfind('/home/$sysop/test.txt')=".json_encode(Vfs::propfind("/home/$sysop/test.txt"), JSON_UNESCAPED_SLASHES)); + +var_dump($f=fopen("vfs://default/home/$sysop/test.txt", 'r'), fread($f, 100), fclose($f)); +//var_dump(Vfs::find("/home/$sysop", ['maxdepth' => 1])); + +Vfs::$is_root = true; +var_dump(file_put_contents("vfs://default/home/$other/test.txt", "Just a test ;)\n")); +var_dump("Vfs::proppatch('/home/$other/test.txt', [['ns' => Vfs::DEFAULT_PROP_NAMESPACE, 'name' => 'test', 'val' => 'something']])=".array2string(Vfs::proppatch("/home/$other/test.txt", [['ns' => Vfs::DEFAULT_PROP_NAMESPACE, 'name' => 'test', 'val' => 'something']])), + "Vfs::propfind('/home/$other/test.txt')=".json_encode(Vfs::propfind("/home/$other/test.txt"), JSON_UNESCAPED_SLASHES)); +var_dump(Vfs::mount("vfs://$other@default/home/$other", "/home/$sysop/$other", false, false)); +Vfs::$is_root = false; + +var_dump(Vfs::mount()); + +var_dump("Vfs::resolve_url('/home/$sysop/$other/test.txt')=".Vfs::resolve_url("/home/$sysop/$other/test.txt")); +var_dump("Vfs::url_stat('/home/$sysop/$other/test.txt')=".json_encode(Vfs::stat("/home/$sysop/$other/test.txt"), JSON_UNESCAPED_SLASHES)); +var_dump("Vfs::is_readable('/home/$sysop/$other/test.txt')=".json_encode(Vfs::is_readable("/home/$sysop/$other/test.txt"))); +var_dump("fopen('vfs://default/home/$sysop/$other/test.txt', 'r')", $f=fopen("vfs://default/home/$sysop/$other/test.txt", 'r'), fread($f, 100), fclose($f)); +var_dump("Vfs::propfind('/home/$sysop/$other/test.txt')", Vfs::propfind("/home/$sysop/$other/test.txt")); +var_dump("Vfs::proppatch('/home/$sysop/$other/test.txt', [['ns' => Vfs::DEFAULT_PROP_NAMESPACE, 'name' => 'test', 'val' => 'something else']])=".array2string(Vfs::proppatch("/home/$sysop/$other/test.txt", [['ns' => Vfs::DEFAULT_PROP_NAMESPACE, 'name' => 'test', 'val' => 'something else']])), + "Vfs::propfind('/home/$sysop/$other/test.txt')=".json_encode(Vfs::propfind("/home/$sysop/$other/test.txt"), JSON_UNESCAPED_SLASHES)); + + +var_dump("Vfs::url_stat('/home/$sysop/$other/test-dir')=".json_encode(Vfs::stat("/home/$sysop/$other/test-dir"))); +var_dump("Vfs::mkdir('/home/$sysop/$other/test-dir')=".json_encode(Vfs::mkdir("/home/$sysop/$other/test-dir"))); +var_dump("Vfs::url_stat('/home/$sysop/$other/test-dir')=".json_encode(Vfs::stat("/home/$sysop/$other/test-dir"), JSON_UNESCAPED_SLASHES)); +var_dump("Vfs::rmdir('/home/$sysop/$other/test-dir')=".json_encode(Vfs::rmdir("/home/$sysop/$other/test-dir"))); +var_dump("Vfs::url_stat('/home/$sysop/$other/test-dir')=".json_encode(Vfs::stat("/home/$sysop/$other/test-dir"))); + +var_dump("Vfs::scandir('/home/$sysop/$other')=".json_encode(Vfs::scandir("/home/$sysop/$other"), JSON_UNESCAPED_SLASHES)); +var_dump("Vfs::remove('/home/$sysop/$other/test.txt')=".json_encode(Vfs::remove("/home/$sysop/$other/test.txt"), JSON_UNESCAPED_SLASHES)); +var_dump("Vfs::scandir('/home/$sysop/$other')=".json_encode(Vfs::scandir("/home/$sysop/$other"), JSON_UNESCAPED_SLASHES));