work-in-progress keeping vfs user in stream context to allow mounting shares

This commit is contained in:
Ralf Becker 2020-09-15 19:57:46 +02:00
parent 39b630d36e
commit 0cbcd7bde0
6 changed files with 341 additions and 98 deletions

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;
@ -865,11 +865,12 @@ class Vfs
{
throw new Exception\WrongParameter('path has to be string, use check_access($path,$check,$stat=null)!');
}
// query stat array, if not given
// if we have no $stat, delegate whole check to vfs stream-wrapper to correctly deal with shares / effective user-ids
if (is_null($stat))
{
if (!isset($vfs)) $vfs = new Vfs\StreamWrapper();
$stat = $vfs->url_stat($path,0);
//$stat = $vfs->url_stat($path,0);
return $vfs->check_access($path, $check);
}
//error_log(__METHOD__."(path=$path||stat[name]={$stat['name']},stat[mode]=".sprintf('%o',$stat['mode']).",$check)");
@ -898,8 +899,9 @@ class Vfs
//error_log(__METHOD__."(path=$path||stat[name]={$stat['name']},stat[mode]=".sprintf('%o',$stat['mode']).",$check) access via other rights!");
return true;
}
if (!isset($user)) $user = self::$user;
// check if there's owner access and we are the owner
if (($stat['mode'] & ($check << 6)) == ($check << 6) && $stat['uid'] && $stat['uid'] == self::$user)
if (($stat['mode'] & ($check << 6)) == ($check << 6) && $stat['uid'] && $stat['uid'] == $user)
{
//error_log(__METHOD__."(path=$path||stat[name]={$stat['name']},stat[mode]=".sprintf('%o',$stat['mode']).",$check) access via owner rights!");
return true;
@ -907,7 +909,7 @@ class Vfs
// 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))
if (($memberships = $GLOBALS['egw']->accounts->memberships($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;
@ -1038,7 +1040,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 +1059,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)
}
/**
@ -2286,9 +2288,10 @@ class Vfs
* @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
* @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
*/
static protected function _call_on_backend($name,$params,$fail_silent=false,$path_param_key=0)
protected static function _call_on_backend($name, array $params, $fail_silent=false, $path_param_key=0, $instanciate=false)
{
$pathes = $params[$path_param_key];
@ -2313,14 +2316,15 @@ class Vfs
if (!$fail_silent) trigger_error("Can't $name for scheme $scheme!\n",E_USER_WARNING);
return false;
}
$callback = [$instanciate ? new $class($url) : $class, $name];
if (!is_array($pathes))
{
$params[$path_param_key] = $url;
return call_user_func_array(array($class,$name),$params);
return call_user_func_array($callback, $params);
}
$params[$path_param_key] = $urls;
if (!is_array($r = call_user_func_array(array($class,$name),$params)))
if (!is_array($r = call_user_func_array($callback, $params)))
{
return $r;
}
@ -2412,7 +2416,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 +2432,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);
}

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)
{
@ -159,7 +158,9 @@ class StreamWrapper extends LinksParent
*/
function url_stat ( $url, $flags )
{
$eacl_check=self::check_extended_acl($url,Vfs::READABLE);
$this->check_set_context($url);
$eacl_check=$this->check_extended_acl($url,Vfs::READABLE);
// return vCard as /.entry
if ( $eacl_check && substr($url,-7) == '/.entry' &&
@ -264,7 +265,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 +336,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 +349,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 +432,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 +447,7 @@ class StreamWrapper extends LinksParent
*/
public static function register()
{
stream_register_wrapper(self::SCHEME, __CLASS__);
stream_wrapper_register(self::SCHEME, __CLASS__);
}
}

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\UserContext;
/**
* 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
*
@ -199,6 +193,8 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface
$this->operation = self::url2operation($url);
$dir = Vfs::dirname($url);
error_log(__METHOD__."('$url', $mode, $options) path=$path, context=".json_encode(stream_context_get_options($this->context)));
$this->opened_path = $opened_path = $path;
$this->opened_mode = $mode = str_replace('b','',$mode); // we are always binary, like every Linux system
$this->opened_stream = null;
@ -210,7 +206,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
!Vfs::check_access($dir,Vfs::WRITABLE, $dir_stat, $this->user)) // 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 +229,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 +272,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' && !Vfs::check_access($url,Vfs::READABLE ,$stat, $this->user) ||// we are not allowed to read
$mode != 'r' && !Vfs::check_access($url,Vfs::WRITABLE, $stat, $this->user)) // or edit it
{
self::_remove_password($url);
$op = $mode == 'r' ? 'read' : 'edited';
@ -355,7 +351,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()),
);
@ -534,6 +530,7 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface
function stream_stat ( )
{
if (self::LOG_LEVEL > 1) error_log(__METHOD__."($this->opened_path)");
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 +555,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 || !Vfs::check_access($dir, Vfs::WRITABLE, $parent_stat, $this->user))
{
self::_remove_password($url);
if (self::LOG_LEVEL) error_log(__METHOD__."($url) permission denied!");
@ -610,14 +607,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)))
!Vfs::check_access($from_dir, Vfs::WRITABLE, $from_dir_stat = $this->url_stat($from_dir, 0), $this->user))
{
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 || !Vfs::check_access($to_dir, Vfs::WRITABLE, $to_dir_stat = $this->url_stat($to_dir, 0), $this->user))
{
self::_remove_password($url_from);
self::_remove_password($url_to);
@ -699,7 +696,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 +730,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 || !Vfs::check_access($parent_path,Vfs::WRITABLE, $parent, $this->user))
{
self::_remove_password($url);
if (self::LOG_LEVEL) error_log(__METHOD__."('$url',$mode,$options) permission denied!");
@ -756,7 +753,7 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface
'fs_mime' => self::DIR_MIME_TYPE,
'fs_created' => self::_pdo_timestamp(time()),
'fs_modified' => self::_pdo_timestamp(time()),
'fs_creator' => Vfs::$user,
'fs_creator' => $this->user,
))))
{
// check if some other process created the directory parallel to us (sqlfs would gives SQL errors later!)
@ -796,7 +793,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)))
!Vfs::check_access($parent, Vfs::WRITABLE, $this->url_stat($parent,0), $this->user))
{
self::_remove_password($url);
$err_msg = __METHOD__."($url,$options) ".(!$stat ? 'not found!' :
@ -911,7 +908,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 +1079,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
!Vfs::check_access($url,Vfs::EXECUTABLE|Vfs::READABLE, $stat, $this->user)) // no access
{
self::_remove_password($url);
$msg = !($stat['mode'] & self::MODE_DIR) && $stat['mime'] != self::DIR_MIME_TYPE ?
@ -1152,6 +1149,12 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface
$path = Vfs::parse_url($url,PHP_URL_PATH);
if (!$this->context)
{
$this->check_set_context($url);
}
error_log(__METHOD__."('$url', $flags) path=$path, context=".json_encode(stream_context_get_options($this->context)));
// webdav adds a trailing slash to dirs, which causes url_stat to NOT find the file otherwise
if ($path != '/' && substr($path,-1) == '/')
{
@ -1177,7 +1180,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 +1211,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
@ -1248,6 +1251,10 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface
if (method_exists($GLOBALS['egw'],'invalidate_session_cache')) $GLOBALS['egw']->invalidate_session_cache();
return $this->url_stat($url, $flags);
}
if (!isset($info['effectiv-uid']) && $this->context)
{
$info['effectiv-uid'] = stream_context_get_options($this->context)[Vfs::SCHEME]['user'];
}
self::$stat_cache[$path] = $info;
if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url,$flags)=".array2string($info));
@ -1259,23 +1266,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 +1348,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 +1364,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)))
!Vfs::check_access($dir,Vfs::WRITABLE, $dir_stat=$this->url_stat($dir,0), $this->user))
{
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 +1389,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 +1412,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 +1437,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));
}
@ -1732,6 +1737,7 @@ GROUP BY A.fs_id';
// eGW addition to return some extra values
'mime' => $info['fs_mime'],
'readlink' => $info['fs_link'],
'effectiv-uid' => $info['effectiv-uid'],
);
if (self::LOG_LEVEL > 1) error_log(__METHOD__."($info[name]) = ".array2string($stat));
return $stat;
@ -1843,14 +1849,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;
}
@ -1910,17 +1914,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;
@ -1974,7 +1975,7 @@ GROUP BY A.fs_id';
*/
public static function register()
{
stream_register_wrapper(self::SCHEME, __CLASS__);
stream_wrapper_register(self::SCHEME, __CLASS__);
}
}

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;
@ -26,6 +25,8 @@ use EGroupware\Api;
*/
class StreamWrapper implements StreamWrapperIface
{
use UserContext;
/**
* Scheme / protocol used for this stream-wrapper
*/
@ -39,12 +40,6 @@ class StreamWrapper implements StreamWrapperIface
*/
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
*/
@ -174,7 +169,12 @@ 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;
@ -304,6 +304,8 @@ class StreamWrapper implements StreamWrapperIface
unset($options,$opened_path); // not used but required by function signature
$this->opened_stream = null;
error_log(__METHOD__."('$path', $mode, $options) context=".json_encode(stream_context_get_options($this->context)));
$stat = null;
if (!($url = $this->resolve_url_symlinks($path,$mode[0]=='r',true,$stat)))
{
@ -313,8 +315,10 @@ class StreamWrapper implements StreamWrapperIface
{
return false;
}
if (!($this->opened_stream = $this->context ?
fopen($url, $mode, false, $this->context) : fopen($url, $mode, false)))
$this->check_set_context($url, true);
if (!($this->opened_stream = $context ?
fopen($url, $mode, false, $context) : fopen($url, $mode, false)))
{
return false;
}
@ -325,7 +329,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 +542,12 @@ class StreamWrapper implements StreamWrapperIface
{
return false;
}
// set user-context
$this->check_set_context($url, true);
$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 +591,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 +651,11 @@ class StreamWrapper implements StreamWrapperIface
{
return false;
}
// set user context
if (Vfs::parse_url($url, PHP_URL_USER))
{
$this->check_set_context($url, true);
}
// check if recursive option is set and needed
if (($options & STREAM_MKDIR_RECURSIVE) &&
($parent_url = Vfs::dirname($url)) &&
@ -660,7 +674,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 +716,13 @@ 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, true);
}
self::symlinkCache_remove($path);
$ok = rmdir($url);
$ok = rmdir($url, $this->context);
// call "vfs_rmdir" hook, only after successful rmdir
if ($ok && !class_exists('setup_process', false))
@ -735,6 +754,9 @@ 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, true);
if (!($this->opened_dir = $this->context ?
opendir($this->opened_dir_url, $this->context) : opendir($this->opened_dir_url)))
{
@ -790,13 +812,22 @@ 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 )
{
// we have no context, but $path is a URL with a valid user --> set it
$this->check_set_context($path);
if (!($url = self::resolve_url($path,!($flags & STREAM_URL_STAT_LINK), $check_symlink_components)))
{
if (self::LOG_LEVEL > 0) error_log(__METHOD__."('$path',$flags) can NOT resolve path!");
return false;
}
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 {
clearstatcache(); // testwise, NOT good for performance
if ($flags & STREAM_URL_STAT_LINK)
{
$stat = @lstat($url); // suppressed the stat failed warnings
@ -853,6 +884,7 @@ class StreamWrapper implements StreamWrapperIface
// if numer of tries is exceeded, re-throw exception
throw $e;
}
clearstatcache(); // testwise, NOT good for performance
// check if a failed url_stat was for a home dir, in that case silently create it
if (!$stat && $try_create_home && Vfs::dirname(Vfs::parse_url($path,PHP_URL_PATH)) == '/home' &&
($id = $GLOBALS['egw']->accounts->name2id(Vfs::basename($path))) &&
@ -901,6 +933,38 @@ class StreamWrapper implements StreamWrapperIface
return $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 path
* @param int $check mode to check: one or more or'ed together of: 4 = self::READABLE,
* 2 = self::WRITABLE, 1 = self::EXECUTABLE
* @return boolean
*/
function check_access($path, $check)
{
if (!($stat = $this->url_stat($path, 0)))
{
$ret = false;
}
else
{
if (($account_lid = Vfs::parse_url($stat['url'], PHP_URL_USER)) &&
($account_id = Api\Accounts::getInstance()->name2id($account_lid)))
{
$user = $account_id;
}
else
{
$user = $this->user ?: Vfs::$user;
}
$ret = Vfs::check_access($path, $check, $stat, $user);
}
error_log(__METHOD__."('$path', $check) user=".Api\Accounts::id2name($user).", effective user=$user=".Api\Accounts::id2name($user).", mode=".decoct($stat['mode'] & 0777).", uid=".($stat['uid']?Api\Accounts::id2name($stat['uid']):0).", uid=".($stat['gid']?Api\Accounts::id2name(-$stat['gid']):0)." returning ".array2string($ret));
return $ret;
}
/**
* Check if path (which fails the stat call) contains symlinks in path-components other then the last one
*
@ -1156,6 +1220,10 @@ class StreamWrapper implements StreamWrapperIface
*/
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
@ -1329,12 +1397,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);
}
}
}

119
api/src/Vfs/UserContext.php Normal file
View File

@ -0,0 +1,119 @@
<?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 UserContext
{
/**
* 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;
}
elseif(is_string($url_or_context))
{
$this->check_set_context($url_or_context, true);
}
}
/**
* Check if we have no user-context, but an url with a user --> set it as context
*
* @param $url
* @param bool $always_set false (default): only set if we have not context or user in context, true: always set
*/
protected function check_set_context($url, $always_set=false)
{
if (($always_set || !$this->context || empty(stream_context_get_options($this->context)[Vfs::SCHEME]['user'])) &&
$url[0] !== '/' && ($account_lid = Vfs::parse_url($url, PHP_URL_USER)))
{
$this->user = $account_lid;
}
}
/**
* @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'] : null;
}
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]
];
if ($this->context)
{
stream_context_set_option($this->context, $options);
}
else
{
$this->context = stream_context_create($options);
}
}
break;
}
}
/**
* @param string $name
* @return bool
*/
public function __isset($name)
{
return $this->__get($name) !== null;
}
}

43
vfs-context.php Normal file
View File

@ -0,0 +1,43 @@
<?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' => 'ralf',
];
var_dump(Vfs\StreamWrapper::mount());
var_dump(file_put_contents('vfs://default/home/ralf/test.txt', "Just a test ;)\n"));
var_dump($f=fopen('vfs://default/home/ralf/test.txt', 'r'), fread($f, 100), fclose($f));
Vfs::$is_root = true;
var_dump(file_put_contents('vfs://default/home/birgit/test.txt', "Just a test ;)\n"));
var_dump(Vfs\StreamWrapper::mount('vfs://birgit@default/home/birgit', '/home/ralf/birgit'));
Vfs::$is_root = false;
var_dump(Vfs\StreamWrapper::mount());
var_dump("Vfs::resolve_url('/home/ralf/birgit/test.txt')=".Vfs::resolve_url('/home/ralf/birgit/test.txt'));
var_dump("Vfs::url_stat('/home/ralf/birgit/test.txt')=".json_encode(Vfs::stat('/home/ralf/birgit/test.txt'), JSON_UNESCAPED_SLASHES));
var_dump("Vfs::is_readable('/home/ralf/birgit/test.txt')=".json_encode(Vfs::is_readable('/home/ralf/birgit/test.txt')));
var_dump("fopen('vfs://default/home/ralf/birgit/test.txt', 'r')", $f=fopen('vfs://default/home/ralf/birgit/test.txt', 'r'), fread($f, 100), fclose($f));
var_dump("Vfs::url_stat('/home/ralf/birgit/test.txt')=".json_encode(Vfs::stat('/home/ralf/birgit/test-dir')));
var_dump("Vfs::mkdir('/home/ralf/birgit/test-dir')=".json_encode(Vfs::mkdir('/home/ralf/birgit/test-dir')));
var_dump("Vfs::url_stat('/home/ralf/birgit/test.txt')=".json_encode(Vfs::stat('/home/ralf/birgit/test-dir'), JSON_UNESCAPED_SLASHES));
var_dump("Vfs::rmdir('/home/ralf/birgit/test-dir')=".json_encode(Vfs::rmdir('/home/ralf/birgit/test-dir')));
var_dump("Vfs::url_stat('/home/ralf/birgit/test.txt')=".json_encode(Vfs::stat('/home/ralf/birgit/test-dir')));
var_dump("Vfs::scandir('/home/ralf/birgit')=".json_encode(Vfs::scandir('/home/ralf/birgit'), JSON_UNESCAPED_SLASHES));
var_dump("Vfs::remove('/home/ralf/birgit/test.txt')=".json_encode(Vfs::remove('/home/ralf/birgit/test.txt')));
var_dump("Vfs::scandir('/home/ralf/birgit')=".json_encode(Vfs::scandir('/home/ralf/birgit'), JSON_UNESCAPED_SLASHES));