work in progress on sharing stream-wrapper

This commit is contained in:
Ralf Becker 2020-09-18 18:45:23 +02:00
parent 2881ca0849
commit 1f8a003d03
6 changed files with 264 additions and 82 deletions

View File

@ -2123,79 +2123,6 @@ class Vfs extends Vfs\Base
return $path[0] == '/' && unlink(self::PREFIX.$path);
}
/**
* Allow to call methods of the underlying stream wrapper: touch, chmod, chgrp, chown, ...
*
* We cant use a magic __call() method, as it does not work for static methods!
*
* @param string $name
* @param array $params first param has to be the path, otherwise we can not determine the correct wrapper
* @param boolean $fail_silent =false should only false be returned if function is not supported by the backend,
* or should an E_USER_WARNING error be triggered (default)
* @param int $path_param_key =0 key in params containing the path, default 0
* @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 = self::resolve_url_symlinks($path,false,false)))
{
return false;
}
$k=(string)self::parse_url($url,PHP_URL_SCHEME);
if (!(is_array($scheme2urls[$k]))) $scheme2urls[$k] = array();
$scheme2urls[$k][$path] = $url;
}
$ret = array();
foreach($scheme2urls as $scheme => $urls)
{
if ($scheme)
{
if (!class_exists($class = self::scheme2class($scheme)) || !method_exists($class,$name))
{
if (!$fail_silent) trigger_error("Can't $name for scheme $scheme!\n",E_USER_WARNING);
return false;
}
$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=self::parse_url($url,PHP_URL_PATH)]))
{
$ret[$path] = $r[$url];
}
}
}
// call the filesystem specific function (dont allow to use arrays!)
elseif(!function_exists($name) || is_array($pathes))
{
return false;
}
else
{
$time = null;
return $name($url,$time);
}
}
return $ret;
}
/**
* touch just running on VFS path
*

View File

@ -498,8 +498,8 @@ class Base
*
* @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 boolean|"null" $fail_silent =false should only false be returned if function is not supported by the backend,
* or should an E_USER_WARNING error be triggered (default), or "null": return NULL
* @param int $path_param_key =0 key in params containing the path, default 0
* @param boolean $instanciate =false true: instanciate the class to call method $name, false: static call
* @return mixed return value of backend or false if function does not exist on backend
@ -527,7 +527,7 @@ class Base
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;
return $fail_silent === 'null' ? null : false;
}
$callback = [$instanciate ? new $class($url) : $class, $name];
if (!is_array($pathes))

View File

@ -0,0 +1,162 @@
<?php
/**
* EGroupware API: VFS - sharing stream wrapper
*
* @link http://www.egroupware.org
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @package api
* @subpackage vfs
* @author Ralf Becker <rb@egroupware.org>
* @copyright (c) 2020 by Ralf Becker <rb@egroupware.org>
*/
namespace EGroupware\Api\Vfs\Sharing;
use EGroupware\Api\Vfs;
use EGroupware\Api;
/**
* VFS - sharing stream wrapper
*
* Sharing stream wrapper allows to mount a share represented by it's hash and optional password to be mounted
* into EGroupware's VFS: sharing://<hash>[:<password>]@default/ --> vfs://<sharee>@default/<shared-path>
*/
class StreamWrapper extends Vfs\StreamWrapper
{
const SCHEME = 'sharing';
const PREFIX = 'sharing://default';
/**
* Resolve the given path according to our fstab
*
* @param string $url
* @param boolean $do_symlink =true is a direct match allowed, default yes (must be false for a lstat or readlink!)
* @param boolean $use_symlinkcache =true
* @param boolean $replace_user_pass_host =true replace $user,$pass,$host in url, default true, if false result is not cached
* @param boolean $fix_url_query =false true append relativ path to url query parameter, default not
* @return string|boolean false if the url cant be resolved, should not happen if fstab has a root entry
*/
static function resolve_url($url, $do_symlink = true, $use_symlinkcache = true, $replace_user_pass_host = true, $fix_url_query = false)
{
$parts = Vfs::parse_url($url);
$hash = $parts['user'] ?: explode('/', $parts['path'])[1];
$rel_path = empty($parts['user']) ? preg_replace('|^/[^/]+|', '', $parts['path']) : $parts['path'];
try
{
if (empty($hash)) throw new Api\Exception\NotFound('Hash must not be empty', 404);
Api\Sharing::check_token(false, $share, $hash, $parts['pass'] ?? '');
if (empty($share['share_owner']) || !($account_lid = Api\Accounts::id2name($share['share_owner'])))
{
throw new Api\Exception\NotFound('Share owner not found', 404);
}
return Vfs::concat('vfs://'.$account_lid.'@default'.Vfs::parse_url($share['share_path'], PHP_URL_PATH), $rel_path).
($share['share_writable'] ? '' : '?ro=1');
}
catch (Api\Exception $e) {
_egw_log_exception($e);
return false;
}
}
/**
* This method is called in response to stat() calls on the URL paths associated with the wrapper.
*
* It should return as many elements in common with the system function as possible.
* Unknown or unavailable values should be set to a rational value (usually 0).
*
* If you plan to use your wrapper in a require_once you need to define stream_stat().
* If you plan to allow any other tests like is_file()/is_dir(), you have to define url_stat().
* stream_stat() must define the size of the file, or it will never be included.
* url_stat() must define mode, or is_file()/is_dir()/is_executable(), and any of those functions affected by clearstatcache() simply won't work.
* It's not documented, but directories must be a mode like 040777 (octal), and files a mode like 0100666.
* If you wish the file to be executable, use 7s instead of 6s.
* The last 3 digits are exactly the same thing as what you pass to chmod.
* 040000 defines a directory, and 0100000 defines a file.
*
* @param string $path
* @param int $flags holds additional flags set by the streams API. It can hold one or more of the following values OR'd together:
* - STREAM_URL_STAT_LINK For resources with the ability to link to other resource (such as an HTTP Location: forward,
* or a filesystem symlink). This flag specified that only information about the link itself should be returned,
* not the resource pointed to by the link.
* This flag is set in response to calls to lstat(), is_link(), or filetype().
* - STREAM_URL_STAT_QUIET If this flag is set, your wrapper should not raise any errors. If this flag is not set,
* you are responsible for reporting errors using the trigger_error() function during stating of the path.
* stat triggers it's own warning anyway, so it makes no sense to trigger one by our stream-wrapper!
* @param boolean $try_create_home =false should a user home-directory be created automatic, if it does not exist
* @param boolean $check_symlink_components =true check if path contains symlinks in path components other then the last one
* @return array
*/
function url_stat ( $path, $flags, $try_create_home=false, $check_symlink_components=true, $check_symlink_depth=self::MAX_SYMLINK_DEPTH, $try_reconnect=true )
{
if (($stat = parent::url_stat($path, $flags, $try_create_home, $check_symlink_components, $check_symlink_depth, $try_reconnect)))
{
$this->check_set_context($stat['url'], true);
}
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 = 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 (!isset($stat)) $stat = $this->url_stat($path, 0);
return $this->parent_check_access($path, $check, $stat);
}
/**
* Store properties for a single ressource (file or dir)
*
* @param string $path string with path
* @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)
*/
function proppatch($path,array $props)
{
if (!($url = self::resolve_url($path)))
{
return false;
}
return Vfs::proppatch($url, $props);
}
/**
* Read properties for a ressource (file, dir or all files of a dir)
*
* @param array|string $path (array of) string with path
* @param string $ns ='http://egroupware.org/' namespace if propfind should be limited to a single one, otherwise use null
* @return array|boolean array with props (values for keys 'name', 'ns', 'val'), or path => array of props for is_array($path)
* false if $path does not exist
*/
function propfind($path,$ns=self::DEFAULT_PROP_NAMESPACE)
{
if (!($url = self::resolve_url($path)))
{
return false;
}
return Vfs::propfind($url, $ns);
}
/**
* Register __CLASS__ for self::SCHEMA
*/
public static function register()
{
stream_wrapper_register(self::SCHEME, __CLASS__);
}
}
StreamWrapper::register();

View File

@ -1,6 +1,6 @@
<?php
/**
* EGroupware API: VFS - stream wrapper interface
* EGroupware API: VFS - stream wrapper
*
* @link http://www.egroupware.org
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
@ -16,7 +16,7 @@ use EGroupware\Api\Vfs;
use EGroupware\Api;
/**
* eGroupWare API: VFS - stream wrapper interface
* VFS - stream wrapper
*
* The new vfs stream wrapper uses a kind of fstab to mount different filesystems / stream wrapper types
* together for eGW's virtual file system.
@ -25,7 +25,9 @@ use EGroupware\Api;
*/
class StreamWrapper extends Base implements StreamWrapperIface
{
use UserContextTrait;
use UserContextTrait {
check_access as parent_check_access;
}
const PREFIX = 'vfs://default';
@ -109,6 +111,26 @@ class StreamWrapper extends Base implements StreamWrapperIface
*/
private $extra_dir_ptr;
/**
* The stream_wrapper interface checks is_{readable|writable|executable} against the webservers uid,
* which is wrong in case of our vfs, as we use the current users id and memberships
*
* @param string $path path
* @param int $check mode to check: one or more or'ed together of: 4 = Vfs::READABLE,
* 2 = Vfs::WRITABLE, 1 = Vfs::EXECUTABLE
* @param array|boolean $stat =null stat array or false, to not query it again
* @return boolean
*/
function check_access($path, $check, $stat=null)
{
$ret = self::_call_on_backend('check_access', [$path, $check, $stat], "null", 0, true);
if (!isset($ret))
{
$ret = $this->parent_check_access($path, $check, $stat);
}
return $ret;
}
/**
* Resolve the given path according to our fstab AND symlinks
*
@ -672,7 +694,7 @@ class StreamWrapper extends Base implements StreamWrapperIface
// 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 (!($url = static::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;
@ -692,7 +714,7 @@ class StreamWrapper extends Base implements StreamWrapperIface
{
$stat = @stat($url); // suppressed the stat failed warnings
if ($stat && ($stat['mode'] & self::MODE_LINK))
if ($stat && ($stat['mode'] & self::MODE_LINK))
{
if (!$check_symlink_depth)
{

View File

@ -44,7 +44,10 @@ trait UserContextTrait
}
else
{
$this->context = stream_context_get_default();
if (!isset($this->context)) // PHP set's it before constructor is called!
{
$this->context = stream_context_get_default();
}
if(is_string($url_or_context))
{

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

@ -0,0 +1,68 @@
<?php
use EGroupware\Api;
use EGroupware\Api\Vfs;
$GLOBALS['egw_info'] = [
'flags' => [
'currentapp' => 'login',
],
];
require_once __DIR__.'/header-default.inc.php';
$GLOBALS['egw_info']['user'] = [
'account_id' => 5,
'account_lid' => $sysop='ralf',
];
$other = 'birgit';
$schema = 'sqlfs';//'stylite.versioning'; //'sqlfs';
Vfs::$is_root = true;
Vfs::mount("$schema://default/home", '/home', false, false);
Vfs::$is_root = false;
var_dump(Vfs::mount());
//var_dump(Vfs::scandir('/home'));
//var_dump(Vfs::find('/home', ['maxdepth' => 1]));
//var_dump(Vfs::scandir("/home/$sysop"));
var_dump(file_put_contents("vfs://default/home/$sysop/test.txt", "Just a test ;)\n"));
var_dump("Vfs::proppatch('/home/$sysop/test.txt', [['ns' => Vfs::DEFAULT_PROP_NAMESPACE, 'name' => 'test', 'val' => 'something']])=".array2string(Vfs::proppatch("/home/$sysop/test.txt", [['ns' => Vfs::DEFAULT_PROP_NAMESPACE, 'name' => 'test', 'val' => 'something']])),
"Vfs::propfind('/home/$sysop/test.txt')=".json_encode(Vfs::propfind("/home/$sysop/test.txt"), JSON_UNESCAPED_SLASHES));
var_dump($f=fopen("vfs://default/home/$sysop/test.txt", 'r'), fread($f, 100), fclose($f));
//var_dump(Vfs::find("/home/$sysop", ['maxdepth' => 1]));
Vfs::$is_root = true;
var_dump(file_put_contents("vfs://default/home/$other/test.txt", "Just a test ;)\n"));
var_dump("Vfs::proppatch('/home/$other/test.txt', [['ns' => Vfs::DEFAULT_PROP_NAMESPACE, 'name' => 'test', 'val' => 'something']])=".array2string(Vfs::proppatch("/home/$other/test.txt", [['ns' => Vfs::DEFAULT_PROP_NAMESPACE, 'name' => 'test', 'val' => 'something']])),
"Vfs::propfind('/home/$other/test.txt')=".json_encode(Vfs::propfind("/home/$other/test.txt"), JSON_UNESCAPED_SLASHES));
$backup = Vfs::$user; Vfs::$user = Api\Accounts::getInstance()->name2id($other);
$share = stylite_sharing::create("/home/$other", stylite_sharing::WRITABLE, '', $sysop);
Vfs::$user = $backup;
var_dump($share);
var_dump(Vfs::mount("sharing://$share[share_token]@default/", "/home/$sysop/$other", false, false));
Vfs::$is_root = false;
var_dump(Vfs::mount());
var_dump("Vfs::resolve_url('/home/$sysop/$other/test.txt')=".Vfs::resolve_url("/home/$sysop/$other/test.txt"));
var_dump("Vfs::url_stat('/home/$sysop/$other/test.txt')=".json_encode(Vfs::stat("/home/$sysop/$other/test.txt"), JSON_UNESCAPED_SLASHES));
var_dump("Vfs::is_readable('/home/$sysop/$other/test.txt')=".json_encode(Vfs::is_readable("/home/$sysop/$other/test.txt")));
var_dump("fopen('vfs://default/home/$sysop/$other/test.txt', 'r')", $f=fopen("vfs://default/home/$sysop/$other/test.txt", 'r'), fread($f, 100), fclose($f));
/* fails
var_dump("Vfs::propfind('/home/$sysop/$other/test.txt')", Vfs::propfind("/home/$sysop/$other/test.txt"));
var_dump("Vfs::proppatch('/home/$sysop/$other/test.txt', [['ns' => Vfs::DEFAULT_PROP_NAMESPACE, 'name' => 'test', 'val' => 'something else']])=".array2string(Vfs::proppatch("/home/$sysop/$other/test.txt", [['ns' => Vfs::DEFAULT_PROP_NAMESPACE, 'name' => 'test', 'val' => 'something else']])),
"Vfs::propfind('/home/$sysop/$other/test.txt')=".json_encode(Vfs::propfind("/home/$sysop/$other/test.txt"), JSON_UNESCAPED_SLASHES));*/
/* mkdir goes into infinit recursion
var_dump("Vfs::url_stat('/home/$sysop/$other/test-dir')=".json_encode(Vfs::stat("/home/$sysop/$other/test-dir")));
var_dump("Vfs::mkdir('/home/$sysop/$other/test-dir')=".json_encode(Vfs::mkdir("/home/$sysop/$other/test-dir")));
var_dump("Vfs::url_stat('/home/$sysop/$other/test-dir')=".json_encode(Vfs::stat("/home/$sysop/$other/test-dir"), JSON_UNESCAPED_SLASHES));
var_dump("Vfs::rmdir('/home/$sysop/$other/test-dir')=".json_encode(Vfs::rmdir("/home/$sysop/$other/test-dir")));
var_dump("Vfs::url_stat('/home/$sysop/$other/test-dir')=".json_encode(Vfs::stat("/home/$sysop/$other/test-dir")));
*/
var_dump("Vfs::scandir('/home/$sysop/$other')=".json_encode(Vfs::scandir("/home/$sysop/$other"), JSON_UNESCAPED_SLASHES));
//var_dump("Vfs::remove('/home/$sysop/$other/test.txt')=".json_encode(Vfs::remove("/home/$sysop/$other/test.txt"), JSON_UNESCAPED_SLASHES));
//var_dump("Vfs::scandir('/home/$sysop/$other')=".json_encode(Vfs::scandir("/home/$sysop/$other"), JSON_UNESCAPED_SLASHES));
stylite_sharing::delete($share['share_id']);