got links-stream-wrapper working and refactored context handling into a trait

This commit is contained in:
Ralf Becker 2020-09-16 15:59:05 +02:00
parent 631a0b11d6
commit 2602157032
10 changed files with 442 additions and 285 deletions

View File

@ -66,33 +66,8 @@ 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;
/**
* Name of the lock table
*/
@ -809,6 +784,7 @@ class Vfs
* @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,77 +831,8 @@ 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)!');
}
// 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);
return $vfs->check_access($path, $check);
}
//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;
}
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'] == $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($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;
return $vfs->check_access($path, $check, $stat);
}
/**

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

@ -0,0 +1,120 @@
<?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\Vfs;
/**
* Shared base of Vfs class and Vfs-stream-wrapper
*/
class Base
{
const PREFIX = 'vfs://default';
/**
* 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;
/**
* 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
* @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 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

@ -95,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';
@ -112,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;
@ -125,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]);
}
/**

View File

@ -35,7 +35,7 @@ use EGroupware\Api;
*/
class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface
{
use Vfs\UserContext;
use Vfs\UserContextTrait;
/**
* Mime type of directories, the old vfs uses 'Directory', while eg. WebDAV uses 'httpd/unix-directory'
@ -206,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, $this->user)) // 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!");
@ -272,8 +272,8 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface
}
else
{
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
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';
@ -555,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, $this->user))
!$dir || !$this->check_access($dir, Vfs::WRITABLE, $parent_stat))
{
self::_remove_password($url);
if (self::LOG_LEVEL) error_log(__METHOD__."($url) permission denied!");
@ -607,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), $this->user))
!$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), $this->user))
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);
@ -730,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, $this->user))
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!");
@ -753,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' => $this->user,
'fs_creator' => Vfs::$user,
))))
{
// check if some other process created the directory parallel to us (sqlfs would gives SQL errors later!)
@ -793,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), $this->user))
!$this->check_access($parent, Vfs::WRITABLE))
{
self::_remove_password($url);
$err_msg = __METHOD__."($url,$options) ".(!$stat ? 'not found!' :
@ -1079,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, $this->user)) // 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 ?
@ -1374,7 +1374,7 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface
return false; // $link already exists
}
if (!($dir = Vfs::dirname($link)) ||
!Vfs::check_access($dir,Vfs::WRITABLE, $dir_stat=$this->url_stat($dir,0), $this->user))
!$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
@ -1864,7 +1864,7 @@ GROUP BY A.fs_id';
{
return false;
}
if (!Vfs::check_access($path,Api\Acl::EDIT, $stat, $this->user))
if (!$this->check_access($path,Api\Acl::EDIT, $stat))
{
return false; // permission denied
}

View File

@ -23,28 +23,15 @@ use EGroupware\Api;
*
* @link http://www.php.net/manual/en/function.stream-wrapper-register.php
*/
class StreamWrapper implements StreamWrapperIface
class StreamWrapper extends Base implements StreamWrapperIface
{
use UserContext;
use UserContextTrait;
/**
* 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';
/**
* Should unreadable entries in a not writable directory be hidden, default yes
*/
const HIDE_UNREADABLES = true;
/**
* mode-bits, which have to be set for links
*/
const MODE_LINK = 0120000;
/**
* How much should be logged to the apache error-log
*
@ -317,8 +304,8 @@ class StreamWrapper implements StreamWrapperIface
}
$this->check_set_context($url, true);
if (!($this->opened_stream = $context ?
fopen($url, $mode, false, $context) : fopen($url, $mode, false)))
if (!($this->opened_stream = $this->context ?
fopen($url, $mode, false, $this->context) : fopen($url, $mode, false)))
{
return false;
}
@ -763,7 +750,7 @@ class StreamWrapper implements StreamWrapperIface
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)
@ -771,7 +758,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);
}
@ -934,35 +921,24 @@ clearstatcache(); // testwise, NOT good for performance
}
/**
* 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
* Check if extendes ACL (stored in eGW's ACL table) grants access
*
* @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
* 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_access($path, $check)
function check_extended_acl($path, $check)
{
if (!($stat = $this->url_stat($path, 0)))
if (!($url = self::resolve_url($path)))
{
$ret = false;
if (self::LOG_LEVEL > 0) error_log(__METHOD__."('$path', $check) can NOT resolve path: ".function_backtrace(1));
return 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 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
}
/**
@ -1123,7 +1099,7 @@ clearstatcache(); // testwise, NOT good for performance
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;

View File

@ -1,119 +0,0 @@
<?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;
}
}

View File

@ -0,0 +1,207 @@
<?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;
}
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;
}
}
/**
* 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
}
// 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;
}
}
// 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 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'] : 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]
];
// 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;
}
}
/**
* @param string $name
* @return bool
*/
public function __isset($name)
{
return $this->__get($name) !== null;
}
}

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

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

@ -0,0 +1,66 @@
<?php
use EGroupware\Api;
use EGroupware\Api\Vfs;
$GLOBALS['egw_info'] = [
'flags' => [
'currentapp' => 'login',
],
];
require_once __DIR__.'/header-default.inc.php';
$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);
foreach(['links', /* 'stylite.links'*/] as $schema)
{
Vfs::$is_root = true;
Vfs\StreamWrapper::mount("$schema://default/apps", '/apps', false, false);
Vfs::$is_root = false;
var_dump(Vfs\StreamWrapper::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\StreamWrapper::mount("$schema://anonymous@default$infolog_anon_dir", $share_dir = '/home/ralf/anon-infolog'));
Vfs::$is_root = false;
var_dump(Vfs\StreamWrapper::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("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));
}

View File

@ -39,12 +39,12 @@ var_dump("Vfs::propfind('/home/ralf/birgit/test.txt')", Vfs::propfind('/home/ral
var_dump("Vfs::proppatch('/home/ralf/birgit/test.txt', [['ns' => Vfs::DEFAULT_PROP_NAMESPACE, 'name' => 'test', 'val' => 'something else']])=".array2string(Vfs::proppatch('/home/ralf/birgit/test.txt', [['ns' => Vfs::DEFAULT_PROP_NAMESPACE, 'name' => 'test', 'val' => 'something else']])),
"Vfs::propfind('/home/ralf/birgit/test.txt')=".json_encode(Vfs::propfind('/home/ralf/birgit/test.txt'), JSON_UNESCAPED_SLASHES));
var_dump("Vfs::url_stat('/home/ralf/birgit/test.txt')=".json_encode(Vfs::stat('/home/ralf/birgit/test-dir')));
var_dump("Vfs::url_stat('/home/ralf/birgit/test-dir')=".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::url_stat('/home/ralf/birgit/test-dir')=".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::url_stat('/home/ralf/birgit/test-dir')=".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::remove('/home/ralf/birgit/test.txt')=".json_encode(Vfs::remove('/home/ralf/birgit/test.txt'), JSON_UNESCAPED_SLASHES));
var_dump("Vfs::scandir('/home/ralf/birgit')=".json_encode(Vfs::scandir('/home/ralf/birgit'), JSON_UNESCAPED_SLASHES));