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 * 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! * 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 * 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 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) * @param int $user =null user used for check, if not current user (self::$user)
* @return boolean * @return boolean
* @todo deprecated or even remove $user parameter and code
*/ */
static function check_access($path, $check, $stat=null, $user=null) static function check_access($path, $check, $stat=null, $user=null)
{ {
@ -855,77 +831,8 @@ class Vfs
return $ret; return $ret;
} }
if (self::$is_root)
{
return true;
}
// throw exception if stat array is used insead of path, can be removed soon
if (is_array($path))
{
throw new 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(); if (!isset($vfs)) $vfs = new Vfs\StreamWrapper();
//$stat = $vfs->url_stat($path,0); return $vfs->check_access($path, $check, $stat);
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;
} }
/** /**

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 $access = !($check & Vfs::WRITABLE); // always grant read access to /apps
$what = '!$app'; $what = '!$app';
} }
elseif (!self::check_app_rights($app)) elseif (!$this->check_app_rights($app))
{ {
$access = false; // user has no access to the $app application $access = false; // user has no access to the $app application
$what = 'no app-rights'; $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}! // 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; $required = $check & Vfs::WRITABLE ? Api\Acl::EDIT : Api\Acl::READ;
$access = 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,".Vfs::$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!!!')); if (self::DEBUG) error_log(__METHOD__."($url,$check) user=".Vfs::$user." ($what) ".($access?"access granted ($app:$id:$rel_path)":'no access!!!'));
return $access; return $access;
@ -125,18 +125,18 @@ class StreamWrapper extends LinksParent
* @param string $app * @param string $app
* @return boolean * @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]); return isset($GLOBALS['egw_info']['user']['apps'][$app]);
} }
static $user_apps = array(); 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 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' * 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+) 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 $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 !($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); self::_remove_password($url);
if (self::LOG_LEVEL) error_log(__METHOD__."($url,$mode,$options) file does not exist or can not be created!"); 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 else
{ {
if ($mode == 'r' && !Vfs::check_access($url,Vfs::READABLE ,$stat, $this->user) ||// we are not allowed to read if ($mode == 'r' && !$this->check_access($url,Vfs::READABLE , $stat) ||// we are not allowed to read
$mode != 'r' && !Vfs::check_access($url,Vfs::WRITABLE, $stat, $this->user)) // or edit it $mode != 'r' && !$this->check_access($url,Vfs::WRITABLE, $stat)) // or edit it
{ {
self::_remove_password($url); self::_remove_password($url);
$op = $mode == 'r' ? 'read' : 'edited'; $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); $this->url_stat($dir, STREAM_URL_STAT_LINK);
if (!$parent_stat || !($stat = $this->url_stat($path,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); self::_remove_password($url);
if (self::LOG_LEVEL) error_log(__METHOD__."($url) permission denied!"); 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); $to_dir = Vfs::dirname($path_to);
if (!($from_stat = $this->url_stat($path_from, 0)) || !$from_dir || 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_from);
self::_remove_password($url_to); self::_remove_password($url_to);
if (self::LOG_LEVEL) error_log(__METHOD__."($url_from,$url_to): $path_from permission denied!"); if (self::LOG_LEVEL) error_log(__METHOD__."($url_from,$url_to): $path_from permission denied!");
return false; // no permission or file does not exist 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_from);
self::_remove_password($url_to); 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); $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); self::_remove_password($url);
if (self::LOG_LEVEL) error_log(__METHOD__."('$url',$mode,$options) permission denied!"); 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_mime' => self::DIR_MIME_TYPE,
'fs_created' => self::_pdo_timestamp(time()), 'fs_created' => self::_pdo_timestamp(time()),
'fs_modified' => 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!) // 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)) || if (!($parent = Vfs::dirname($path)) ||
!($stat = $this->url_stat($path, 0)) || $stat['mime'] != self::DIR_MIME_TYPE || !($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); self::_remove_password($url);
$err_msg = __METHOD__."($url,$options) ".(!$stat ? 'not found!' : $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 if (!($stat = $this->url_stat($url,0)) || // dir not found
!($stat['mode'] & self::MODE_DIR) && $stat['mime'] != self::DIR_MIME_TYPE || // no dir !($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); self::_remove_password($url);
$msg = !($stat['mode'] & self::MODE_DIR) && $stat['mime'] != self::DIR_MIME_TYPE ? $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 return false; // $link already exists
} }
if (!($dir = Vfs::dirname($link)) || 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).")"); 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 return false; // parent dir does not exist or is not writable
@ -1864,7 +1864,7 @@ GROUP BY A.fs_id';
{ {
return false; 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 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 * @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 * Should unreadable entries in a not writable directory be hidden, default yes
*/ */
const HIDE_UNREADABLES = true; 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 * How much should be logged to the apache error-log
* *
@ -317,8 +304,8 @@ class StreamWrapper implements StreamWrapperIface
} }
$this->check_set_context($url, true); $this->check_set_context($url, true);
if (!($this->opened_stream = $context ? if (!($this->opened_stream = $this->context ?
fopen($url, $mode, false, $context) : fopen($url, $mode, false))) fopen($url, $mode, false, $this->context) : fopen($url, $mode, false)))
{ {
return 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!"); if (self::LOG_LEVEL > 0) error_log(__METHOD__."( $path,$options) opendir($this->opened_dir_url) failed!");
return false; 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 // check our fstab if we need to add some of the mountpoints
$basepath = Vfs::parse_url($path,PHP_URL_PATH); $basepath = Vfs::parse_url($path,PHP_URL_PATH);
foreach(array_keys(self::$fstab) as $mounted) 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 != '/') && if (((Vfs::dirname($mounted) == $basepath || Vfs::dirname($mounted).'/' == $basepath) && $mounted != '/') &&
// only return children readable by the user, if dir is not writable // only return children readable by the user, if dir is not writable
(!self::HIDE_UNREADABLES || $this->opened_dir_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); $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, * Check if extendes ACL (stored in eGW's ACL table) grants access
* which is wrong in case of our vfs, as we use the current users id and memberships
* *
* @param string $path path * The extended ACL is inherited, so it's valid for all subdirs and the included files!
* @param int $check mode to check: one or more or'ed together of: 4 = self::READABLE, * The used algorithm break on the first match. It could be used, to disallow further access.
* 2 = self::WRITABLE, 1 = self::EXECUTABLE *
* @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 * @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 // 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
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;
} }
/** /**
@ -1123,7 +1099,7 @@ clearstatcache(); // testwise, NOT good for performance
while($file !== false && while($file !== false &&
(is_array($this->extra_dirs) && in_array($file,$this->extra_dirs) || // do NOT return extra_dirs twice (is_array($this->extra_dirs) && in_array($file,$this->extra_dirs) || // do NOT return extra_dirs twice
self::HIDE_UNREADABLES && !$this->opened_dir_writable && 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'"); if (self::LOG_LEVEL > 1) error_log(__METHOD__."( $this->opened_dir ) = '$file'");
return $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) 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']) || (empty($GLOBALS['egw_info']['user']['preferences']['filemanager']['document_doubleclick_action']) ||
$GLOBALS['egw_info']['user']['preferences']['filemanager']['document_doubleclick_action'] == $app)) $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']])), 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)); "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::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::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::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)); var_dump("Vfs::scandir('/home/ralf/birgit')=".json_encode(Vfs::scandir('/home/ralf/birgit'), JSON_UNESCAPED_SLASHES));