change sharing to allow mounting shared into existing user sessions

without the need to destroy the session, if the sharee is a different user

This is accomplished by keeping the sharee in the stream context / attribute of the vfs class(es) instead of static Vfs::$user.
Later is still used for the current user - identical to egw_info[user][account_id].

This commit / merge of the vfs-context feature branch also added the abilty to mount WebDAV sources, eg. a sharing link from an other EGroupware instance
or any WebDAV server
This commit is contained in:
Ralf Becker 2020-10-08 11:24:34 +02:00
parent 7ae230356f
commit 4886583cc1
29 changed files with 2687 additions and 1398 deletions

View File

@ -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;

View File

@ -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();

View File

@ -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();

View File

@ -7,7 +7,7 @@
* @package api
* @subpackage vfs
* @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
* @copyright (c) 2008-19 by Ralf Becker <RalfBecker-AT-outdoor-training.de>
* @copyright (c) 2008-20 by Ralf Becker <RalfBecker-AT-outdoor-training.de>
*/
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:
* <code>
* - 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
* </code>
* @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,25 +1613,26 @@ 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;
}
$path = self::parse_url($url, PHP_URL_PATH);
if (($ret = self::$db->delete(self::LOCK_TABLE,array(
'lock_path' => $path,
'lock_token' => $token,
@ -1773,21 +1641,22 @@ class Vfs
// 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'));
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);
}
/**

578
api/src/Vfs/Base.php Normal file
View File

@ -0,0 +1,578 @@
<?php
/**
* EGroupware API: VFS - shared base of Vfs class and Vfs-stream-wrapper
*
* @link https://www.egroupware.org
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @package api
* @subpackage vfs
* @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
* @copyright (c) 2008-20 by Ralf Becker <RalfBecker-AT-outdoor-training.de>
*/
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;
}
}

View File

@ -1,14 +1,13 @@
<?php
/**
* eGroupWare API: VFS - stream wrapper for linked files
* EGroupware API: VFS - stream wrapper for linked files
*
* @link http://www.egroupware.org
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @package api
* @subpackage vfs
* @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
* @copyright (c) 2008-16 by Ralf Becker <RalfBecker-AT-outdoor-training.de>
* @version $Id: class.sqlfs_stream_wrapper.inc.php 24997 2008-03-02 21:44:15Z ralfbecker $
* @copyright (c) 2008-20 by Ralf Becker <RalfBecker-AT-outdoor-training.de>
*/
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,17 +158,20 @@ class StreamWrapper extends LinksParent
*/
function url_stat ( $url, $flags )
{
$eacl_check=self::check_extended_acl($url,Vfs::READABLE);
$this->check_set_context($url);
$ret = false;
if (($eacl_check = $this->check_extended_acl($url,Vfs::READABLE)))
{
// return vCard as /.entry
if ( $eacl_check && substr($url,-7) == '/.entry' &&
if (substr($url, -7) == '/.entry' &&
(list($app) = array_slice(explode('/', $url), -3, 1)) && $app === 'addressbook')
{
$ret = array(
'ino' => '#' . md5($url),
'name' => '.entry',
'mode' => self::MODE_FILE | Vfs::READABLE, // required by the stream wrapper
'size' => 1024, // fmail does NOT attach files with size 0!
'size' => 1024, // email does NOT attach files with size 0!
'uid' => 0,
'gid' => 0,
'mtime' => time(),
@ -180,7 +182,7 @@ class StreamWrapper extends LinksParent
);
}
// if entry directory does not exist --> return fake directory
elseif (!($ret = parent::url_stat($url,$flags)) && $eacl_check)
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))
@ -200,7 +202,8 @@ class StreamWrapper extends LinksParent
);
}
}
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__);
}
}

View File

@ -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,25 +108,45 @@ 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
{
// 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.
@ -126,26 +156,16 @@ class Sharing extends \EGroupware\Api\Sharing
'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'))
// 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'))
{
$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'];
}
}
}

View File

@ -0,0 +1,124 @@
<?php
/**
* EGroupware API: VFS - sharing stream wrapper
*
* @link http://www.egroupware.org
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @package api
* @subpackage vfs
* @author Ralf Becker <rb@egroupware.org>
* @copyright (c) 2020 by Ralf Becker <rb@egroupware.org>
*/
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://<hash>[:<password>]@default/ --> vfs://<sharee>@default/<shared-path>
*/
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();

View File

@ -7,8 +7,7 @@
* @package api
* @subpackage vfs
* @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
* @copyright (c) 2008-16 by Ralf Becker <RalfBecker-AT-outdoor-training.de>
* @version $Id$
* @copyright (c) 2008-20 by Ralf Becker <RalfBecker-AT-outdoor-training.de>
*/
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__);
}
}

View File

@ -1,14 +1,13 @@
<?php
/**
* EGroupware API: VFS - stream wrapper interface
* EGroupware API: VFS - stream wrapper
*
* @link http://www.egroupware.org
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @package api
* @subpackage vfs
* @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
* @copyright (c) 2008-16 by Ralf Becker <RalfBecker-AT-outdoor-training.de>
* @version $Id$
* @copyright (c) 2008-20 by Ralf Becker <RalfBecker-AT-outdoor-training.de>
*/
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);
}
}
}

View File

@ -0,0 +1,247 @@
<?php
/**
* EGroupware API: VFS - Trait to store user / account_id in stream context
*
* @link https://www.egroupware.org
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @package api
* @subpackage vfs
* @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
* @copyright (c) 2020 by Ralf Becker <RalfBecker-AT-outdoor-training.de>
*/
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;
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,58 @@
<?php
/**
* Test the basic Vfs::StreamWrapper
*
* @link http://www.egroupware.org
* @author Nathan Gray
* @copyright (c) 2020 Nathan Gray
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
*/
namespace EGroupware\Api\Vfs\Filesystem;
require_once __DIR__ . '/../StreamWrapperBase.php';
use EGroupware\Api;
use EGroupware\Api\Vfs;
class StreamWrapperTest extends Vfs\StreamWrapperBase
{
public static $mountpoint = '/home/demo/filesystem';
protected function setUp() : void
{
parent::setUp();
$this->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);
}
}

View File

@ -0,0 +1,113 @@
<?php
/**
* Test the basic Vfs::StreamWrapper
*
* @link http://www.egroupware.org
* @author Nathan Gray
* @copyright (c) 2020 Nathan Gray
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
*/
namespace EGroupware\Api\Vfs\Links;
require_once __DIR__ . '/../StreamWrapperBase.php';
use EGroupware\Api;
use EGroupware\Api\Vfs;
class StreamWrapperTest extends Vfs\StreamWrapperBase
{
protected $entries = [];
protected function setUp() : void
{
parent::setUp();
}
protected function tearDown() : void
{
// Do local stuff first, parent will remove stuff that is needed
$bo = new \infolog_bo();
foreach($this->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';
}
}

View File

@ -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';
}
}

View File

@ -0,0 +1,117 @@
<?php
/**
* Test the basics of Sharing::StreamWrapper
*
* @link http://www.egroupware.org
* @author Nathan Gray
* @copyright (c) 2020 Nathan Gray
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
*/
namespace EGroupware\Api\Vfs\Sharing;
require_once __DIR__ . '/../StreamWrapperBase.php';
use EGroupware\Api;
use EGroupware\Api\Vfs;
use EGroupware\Api\Vfs\Sharing;
class StreamWrapperTest extends Vfs\StreamWrapperBase
{
protected $share = [];
static $test_dir = 'TestShareFolder';
protected function setUp() : void
{
$this->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);
}
}

View File

@ -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);
}
}

View File

@ -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();

View File

@ -77,14 +77,15 @@ class SharingBase extends LoggedInTest
protected function tearDown() : void
{
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();

View File

@ -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;
}
}

View File

@ -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"
);
}
}

View File

@ -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",

657
composer.lock generated
View File

@ -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"
}
}

View File

@ -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))
{

View File

@ -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();
}

68
vfs-context-links.php Normal file
View File

@ -0,0 +1,68 @@
<?php
use EGroupware\Api;
use EGroupware\Api\Vfs;
$GLOBALS['egw_info'] = [
'flags' => [
'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));

72
vfs-context-share.php Normal file
View File

@ -0,0 +1,72 @@
<?php
use EGroupware\Api;
use EGroupware\Api\Vfs;
$GLOBALS['egw_info'] = [
'flags' => [
'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']);

40
vfs-context-webdav.php Normal file
View File

@ -0,0 +1,40 @@
<?php
use EGroupware\Api\Vfs;
use Grale\WebDav;
$GLOBALS['egw_info'] = [
'flags' => [
'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));

66
vfs-context.php Normal file
View File

@ -0,0 +1,66 @@
<?php
use EGroupware\Api\Vfs;
$GLOBALS['egw_info'] = [
'flags' => [
'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));