diff --git a/api/src/Vfs.php b/api/src/Vfs.php index 1331866e1b..97a0c203c6 100644 --- a/api/src/Vfs.php +++ b/api/src/Vfs.php @@ -7,7 +7,7 @@ * @package api * @subpackage vfs * @author Ralf Becker - * @copyright (c) 2008-19 by Ralf Becker + * @copyright (c) 2008-20 by Ralf Becker */ namespace EGroupware\Api; @@ -865,11 +865,12 @@ class Vfs { throw new Exception\WrongParameter('path has to be string, use check_access($path,$check,$stat=null)!'); } - // query stat array, if not given + // if we have no $stat, delegate whole check to vfs stream-wrapper to correctly deal with shares / effective user-ids if (is_null($stat)) { if (!isset($vfs)) $vfs = new Vfs\StreamWrapper(); - $stat = $vfs->url_stat($path,0); + //$stat = $vfs->url_stat($path,0); + return $vfs->check_access($path, $check); } //error_log(__METHOD__."(path=$path||stat[name]={$stat['name']},stat[mode]=".sprintf('%o',$stat['mode']).",$check)"); @@ -898,8 +899,9 @@ class Vfs //error_log(__METHOD__."(path=$path||stat[name]={$stat['name']},stat[mode]=".sprintf('%o',$stat['mode']).",$check) access via other rights!"); return true; } + if (!isset($user)) $user = self::$user; // check if there's owner access and we are the owner - if (($stat['mode'] & ($check << 6)) == ($check << 6) && $stat['uid'] && $stat['uid'] == self::$user) + if (($stat['mode'] & ($check << 6)) == ($check << 6) && $stat['uid'] && $stat['uid'] == $user) { //error_log(__METHOD__."(path=$path||stat[name]={$stat['name']},stat[mode]=".sprintf('%o',$stat['mode']).",$check) access via owner rights!"); return true; @@ -907,7 +909,7 @@ class Vfs // check if there's a group access and we have the right membership if (($stat['mode'] & ($check << 3)) == ($check << 3) && $stat['gid']) { - if (($memberships = $GLOBALS['egw']->accounts->memberships(self::$user, true)) && in_array(-abs($stat['gid']), $memberships)) + if (($memberships = $GLOBALS['egw']->accounts->memberships($user, true)) && in_array(-abs($stat['gid']), $memberships)) { //error_log(__METHOD__."(path=$path||stat[name]={$stat['name']},stat[mode]=".sprintf('%o',$stat['mode']).",$check) access via group rights!"); return true; @@ -1038,7 +1040,7 @@ class Vfs */ static function proppatch($path,array $props) { - return self::_call_on_backend('proppatch',array($path,$props)); + return self::_call_on_backend('proppatch', [$path,$props], false, 0, true); } /** @@ -1057,7 +1059,7 @@ class Vfs */ static function propfind($path,$ns=self::DEFAULT_PROP_NAMESPACE) { - return self::_call_on_backend('propfind',array($path,$ns),true); // true = fail silent (no PHP Warning) + return self::_call_on_backend('propfind', [$path, $ns],true, 0, true); // true = fail silent (no PHP Warning) } /** @@ -2286,9 +2288,10 @@ class Vfs * @param boolean $fail_silent =false should only false be returned if function is not supported by the backend, * or should an E_USER_WARNING error be triggered (default) * @param int $path_param_key =0 key in params containing the path, default 0 + * @param boolean $instanciate =false true: instanciate the class to call method $name, false: static call * @return mixed return value of backend or false if function does not exist on backend */ - static protected function _call_on_backend($name,$params,$fail_silent=false,$path_param_key=0) + protected static function _call_on_backend($name, array $params, $fail_silent=false, $path_param_key=0, $instanciate=false) { $pathes = $params[$path_param_key]; @@ -2313,14 +2316,15 @@ class Vfs if (!$fail_silent) trigger_error("Can't $name for scheme $scheme!\n",E_USER_WARNING); return false; } + $callback = [$instanciate ? new $class($url) : $class, $name]; if (!is_array($pathes)) { $params[$path_param_key] = $url; - return call_user_func_array(array($class,$name),$params); + return call_user_func_array($callback, $params); } $params[$path_param_key] = $urls; - if (!is_array($r = call_user_func_array(array($class,$name),$params))) + if (!is_array($r = call_user_func_array($callback, $params))) { return $r; } @@ -2412,7 +2416,7 @@ class Vfs */ static function readlink($path) { - $ret = self::_call_on_backend('readlink',array($path),true); // true = fail silent, if backend does not support readlink + $ret = self::_call_on_backend('readlink', [$path],true, 0, true); // true = fail silent, if backend does not support readlink //error_log(__METHOD__."('$path') returning ".array2string($ret).' '.function_backtrace()); return $ret; } @@ -2428,7 +2432,7 @@ class Vfs */ static function symlink($target,$link) { - if (($ret = self::_call_on_backend('symlink',array($target,$link),false,1))) // 1=path is in $link! + if (($ret = self::_call_on_backend('symlink', [$target, $link],false,1, true))) // 1=path is in $link! { Vfs\StreamWrapper::symlinkCache_remove($link); } diff --git a/api/src/Vfs/Links/StreamWrapper.php b/api/src/Vfs/Links/StreamWrapper.php index b8e4e7d1dc..feb42fa571 100644 --- a/api/src/Vfs/Links/StreamWrapper.php +++ b/api/src/Vfs/Links/StreamWrapper.php @@ -1,14 +1,13 @@ - * @copyright (c) 2008-16 by Ralf Becker - * @version $Id: class.sqlfs_stream_wrapper.inc.php 24997 2008-03-02 21:44:15Z ralfbecker $ + * @copyright (c) 2008-20 by Ralf Becker */ namespace EGroupware\Api\Vfs\Links; @@ -76,7 +75,7 @@ class StreamWrapper extends LinksParent * @param int $check mode to check: one or more or'ed together of: 4 = read, 2 = write, 1 = executable * @return boolean */ - static function check_extended_acl($url,$check) + function check_extended_acl($url,$check) { if (Vfs::$is_root) { @@ -159,7 +158,9 @@ class StreamWrapper extends LinksParent */ function url_stat ( $url, $flags ) { - $eacl_check=self::check_extended_acl($url,Vfs::READABLE); + $this->check_set_context($url); + + $eacl_check=$this->check_extended_acl($url,Vfs::READABLE); // return vCard as /.entry if ( $eacl_check && substr($url,-7) == '/.entry' && @@ -264,7 +265,7 @@ class StreamWrapper extends LinksParent list(,$apps,$app,$id) = explode('/',$path); $ret = false; - if ($apps == 'apps' && $app && !$id || self::check_extended_acl($path,Vfs::WRITABLE)) // app directory itself is allways ok + if ($apps == 'apps' && $app && !$id || $this->check_extended_acl($path,Vfs::WRITABLE)) // app directory itself is allways ok { $current_is_root = Vfs::$is_root; Vfs::$is_root = true; $current_user = Vfs::$user; Vfs::$user = 0; @@ -335,7 +336,7 @@ class StreamWrapper extends LinksParent { $charset = 'utf-8'; } - if (!($vcard =& $ab_vcard->getVCard($id, $charset))) + if (!($vcard = $ab_vcard->getVCard($id, $charset))) { error_log(__METHOD__."('$url', '$mode', $options) addressbook_vcal::getVCard($id) returned false!"); return false; @@ -348,7 +349,7 @@ class StreamWrapper extends LinksParent } // create not existing entry directories on the fly if ($mode[0] != 'r' && ($dir = Vfs::dirname($url)) && - !parent::url_stat($dir, 0) && self::check_extended_acl($dir, Vfs::WRITABLE)) + !parent::url_stat($dir, 0) && $this->check_extended_acl($dir, Vfs::WRITABLE)) { $this->mkdir($dir,0,STREAM_MKDIR_RECURSIVE); } @@ -431,14 +432,14 @@ class StreamWrapper extends LinksParent * @param string $link * @return boolean true on success false on error */ - static function symlink($target,$link) + function symlink($target,$link) { - $parent = new \EGroupware\Api\Vfs\Links\LinksParent(); - if (!$parent->url_stat($dir = Vfs::dirname($link),0) && self::check_extended_acl($dir,Vfs::WRITABLE)) + $parent = new \EGroupware\Api\Vfs\Links\LinksParent($target); + if (!$parent->url_stat($dir = Vfs::dirname($link),0) && $this->check_extended_acl($dir,Vfs::WRITABLE)) { $parent->mkdir($dir,0,STREAM_MKDIR_RECURSIVE); } - return parent::symlink($target,$link); + return $parent->symlink($target,$link); } /** @@ -446,7 +447,7 @@ class StreamWrapper extends LinksParent */ public static function register() { - stream_register_wrapper(self::SCHEME, __CLASS__); + stream_wrapper_register(self::SCHEME, __CLASS__); } } diff --git a/api/src/Vfs/Sqlfs/StreamWrapper.php b/api/src/Vfs/Sqlfs/StreamWrapper.php index fc3f2f946f..f4f1f2b69a 100644 --- a/api/src/Vfs/Sqlfs/StreamWrapper.php +++ b/api/src/Vfs/Sqlfs/StreamWrapper.php @@ -7,8 +7,7 @@ * @package api * @subpackage vfs * @author Ralf Becker - * @copyright (c) 2008-16 by Ralf Becker - * @version $Id$ + * @copyright (c) 2008-20 by Ralf Becker */ namespace EGroupware\Api\Vfs\Sqlfs; @@ -36,6 +35,8 @@ use EGroupware\Api; */ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface { + use Vfs\UserContext; + /** * Mime type of directories, the old vfs uses 'Directory', while eg. WebDAV uses 'httpd/unix-directory' */ @@ -102,13 +103,6 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface */ protected $operation = self::DEFAULT_OPERATION; - /** - * optional context param when opening the stream, null if no context passed - * - * @var mixed - */ - var $context; - /** * Path off the file opened by stream_open * @@ -199,6 +193,8 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface $this->operation = self::url2operation($url); $dir = Vfs::dirname($url); + error_log(__METHOD__."('$url', $mode, $options) path=$path, context=".json_encode(stream_context_get_options($this->context))); + $this->opened_path = $opened_path = $path; $this->opened_mode = $mode = str_replace('b','',$mode); // we are always binary, like every Linux system $this->opened_stream = null; @@ -210,7 +206,7 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface if (!$dir || $mode[0] == 'r' || // does $mode require the file to exist (r,r+) $mode[0] == 'x' && $stat || // or file should not exist, but does !($dir_stat=$this->url_stat($dir,STREAM_URL_STAT_QUIET)) || // or parent dir does not exist create it - !Vfs::check_access($dir,Vfs::WRITABLE,$dir_stat)) // or we are not allowed to create it + !Vfs::check_access($dir,Vfs::WRITABLE, $dir_stat, $this->user)) // or we are not allowed to create it { self::_remove_password($url); if (self::LOG_LEVEL) error_log(__METHOD__."($url,$mode,$options) file does not exist or can not be created!"); @@ -233,12 +229,12 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface // we use the mode of the dir, so files in group dirs stay accessible by all members 'fs_mode' => $dir_stat['mode'] & 0666, // for the uid we use the uid of the dir if not 0=root or the current user otherwise - 'fs_uid' => $dir_stat['uid'] ? $dir_stat['uid'] : Vfs::$user, + 'fs_uid' => $dir_stat['uid'] ? $dir_stat['uid'] : $this->user, // we allways use the group of the dir 'fs_gid' => $dir_stat['gid'], 'fs_created' => self::_pdo_timestamp(time()), 'fs_modified' => self::_pdo_timestamp(time()), - 'fs_creator' => Vfs::$user, + 'fs_creator' => Vfs::$user, // real user, not effective one / $this->user 'fs_mime' => 'application/octet-stream', // required NOT NULL! 'fs_size' => 0, 'fs_active' => self::_pdo_boolean(true), @@ -276,8 +272,8 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface } else { - if ($mode == 'r' && !Vfs::check_access($url,Vfs::READABLE ,$stat) ||// we are not allowed to read - $mode != 'r' && !Vfs::check_access($url,Vfs::WRITABLE,$stat)) // or edit it + if ($mode == 'r' && !Vfs::check_access($url,Vfs::READABLE ,$stat, $this->user) ||// we are not allowed to read + $mode != 'r' && !Vfs::check_access($url,Vfs::WRITABLE, $stat, $this->user)) // or edit it { self::_remove_password($url); $op = $mode == 'r' ? 'read' : 'edited'; @@ -355,7 +351,7 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface // todo: analyse the file for the mime-type 'fs_mime' => Api\MimeMagic::filename2mime($this->opened_path), 'fs_id' => $this->opened_fs_id, - 'fs_modifier' => Vfs::$user, + 'fs_modifier' => $this->user, 'fs_modified' => self::_pdo_timestamp(time()), ); @@ -534,6 +530,7 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface function stream_stat ( ) { if (self::LOG_LEVEL > 1) error_log(__METHOD__."($this->opened_path)"); + error_log(__METHOD__."() opened_path=$this->opened_path, context=".json_encode(stream_context_get_options($this->context))); return $this->url_stat($this->opened_path,0); } @@ -558,7 +555,7 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface $this->url_stat($dir, STREAM_URL_STAT_LINK); if (!$parent_stat || !($stat = $this->url_stat($path,STREAM_URL_STAT_LINK)) || - !$dir || !Vfs::check_access($dir, Vfs::WRITABLE, $parent_stat)) + !$dir || !Vfs::check_access($dir, Vfs::WRITABLE, $parent_stat, $this->user)) { self::_remove_password($url); if (self::LOG_LEVEL) error_log(__METHOD__."($url) permission denied!"); @@ -610,14 +607,14 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface $to_dir = Vfs::dirname($path_to); if (!($from_stat = $this->url_stat($path_from, 0)) || !$from_dir || - !Vfs::check_access($from_dir, Vfs::WRITABLE, $from_dir_stat = $this->url_stat($from_dir, 0))) + !Vfs::check_access($from_dir, Vfs::WRITABLE, $from_dir_stat = $this->url_stat($from_dir, 0), $this->user)) { self::_remove_password($url_from); self::_remove_password($url_to); if (self::LOG_LEVEL) error_log(__METHOD__."($url_from,$url_to): $path_from permission denied!"); return false; // no permission or file does not exist } - if (!$to_dir || !Vfs::check_access($to_dir, Vfs::WRITABLE, $to_dir_stat = $this->url_stat($to_dir, 0))) + if (!$to_dir || !Vfs::check_access($to_dir, Vfs::WRITABLE, $to_dir_stat = $this->url_stat($to_dir, 0), $this->user)) { self::_remove_password($url_from); self::_remove_password($url_to); @@ -699,7 +696,7 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface if (self::LOG_LEVEL > 1) error_log(__METHOD__." called from:".function_backtrace()); $path = Vfs::parse_url($url,PHP_URL_PATH); - if ($this->url_stat($path,STREAM_URL_STAT_QUIET)) + if ($this->url_stat($url,STREAM_URL_STAT_QUIET)) { self::_remove_password($url); if (self::LOG_LEVEL) error_log(__METHOD__."('$url',$mode,$options) already exist!"); @@ -733,7 +730,7 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface } $parent = $this->url_stat($parent_path,0); } - if (!$parent || !Vfs::check_access($parent_path,Vfs::WRITABLE,$parent)) + if (!$parent || !Vfs::check_access($parent_path,Vfs::WRITABLE, $parent, $this->user)) { self::_remove_password($url); if (self::LOG_LEVEL) error_log(__METHOD__."('$url',$mode,$options) permission denied!"); @@ -756,7 +753,7 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface 'fs_mime' => self::DIR_MIME_TYPE, 'fs_created' => self::_pdo_timestamp(time()), 'fs_modified' => self::_pdo_timestamp(time()), - 'fs_creator' => Vfs::$user, + 'fs_creator' => $this->user, )))) { // check if some other process created the directory parallel to us (sqlfs would gives SQL errors later!) @@ -796,7 +793,7 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface if (!($parent = Vfs::dirname($path)) || !($stat = $this->url_stat($path, 0)) || $stat['mime'] != self::DIR_MIME_TYPE || - !Vfs::check_access($parent, Vfs::WRITABLE, $this->url_stat($parent,0))) + !Vfs::check_access($parent, Vfs::WRITABLE, $this->url_stat($parent,0), $this->user)) { self::_remove_password($url); $err_msg = __METHOD__."($url,$options) ".(!$stat ? 'not found!' : @@ -911,7 +908,7 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface return $stmt->execute(array( 'fs_modified' => self::_pdo_timestamp($time ? $time : time()), - 'fs_modifier' => Vfs::$user, + 'fs_modifier' => $this->user, 'fs_id' => $stat['ino'], )); } @@ -1082,7 +1079,7 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface if (!($stat = $this->url_stat($url,0)) || // dir not found !($stat['mode'] & self::MODE_DIR) && $stat['mime'] != self::DIR_MIME_TYPE || // no dir - !Vfs::check_access($url,Vfs::EXECUTABLE|Vfs::READABLE,$stat)) // no access + !Vfs::check_access($url,Vfs::EXECUTABLE|Vfs::READABLE, $stat, $this->user)) // no access { self::_remove_password($url); $msg = !($stat['mode'] & self::MODE_DIR) && $stat['mime'] != self::DIR_MIME_TYPE ? @@ -1152,6 +1149,12 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface $path = Vfs::parse_url($url,PHP_URL_PATH); + if (!$this->context) + { + $this->check_set_context($url); + } + error_log(__METHOD__."('$url', $flags) path=$path, context=".json_encode(stream_context_get_options($this->context))); + // webdav adds a trailing slash to dirs, which causes url_stat to NOT find the file otherwise if ($path != '/' && substr($path,-1) == '/') { @@ -1177,7 +1180,7 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface $parts = explode('/',$path); // if we have extended acl access to the url, we dont need and can NOT include the sql for the readable check - $eacl_access = static::check_extended_acl($path,Vfs::READABLE); + $eacl_access = $this->check_extended_acl($path,Vfs::READABLE); try { foreach($parts as $n => $name) @@ -1208,13 +1211,13 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface // if we are not root AND have no extended acl access, we need to make sure the user has the right to tranverse all parent directories (read-rights) if (!Vfs::$is_root && !$eacl_access) { - if (!Vfs::$user) + if (!$this->user) { self::_remove_password($url); if (self::LOG_LEVEL > 1) error_log(__METHOD__."('$url',$flags) permission denied, no user-id and not root!"); return false; } - $query .= ' AND '.self::_sql_readable(); + $query .= ' AND '.$this->_sql_readable(); } } else @@ -1248,6 +1251,10 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface if (method_exists($GLOBALS['egw'],'invalidate_session_cache')) $GLOBALS['egw']->invalidate_session_cache(); return $this->url_stat($url, $flags); } + if (!isset($info['effectiv-uid']) && $this->context) + { + $info['effectiv-uid'] = stream_context_get_options($this->context)[Vfs::SCHEME]['user']; + } self::$stat_cache[$path] = $info; if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url,$flags)=".array2string($info)); @@ -1259,23 +1266,23 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface * * @return string */ - protected static function _sql_readable() + protected function _sql_readable() { - static $sql_read_acl=null; + static $sql_read_acl=[]; - if (is_null($sql_read_acl)) + if (!isset($sql_read_acl[$user = $this->user])) { - foreach($GLOBALS['egw']->accounts->memberships(Vfs::$user,true) as $gid) + foreach($GLOBALS['egw']->accounts->memberships($user, true) as $gid) { $memberships[] = abs($gid); // sqlfs stores the gid's positiv } // using octal numbers with mysql leads to funny results (select 384 & 0400 --> 384 not 256=0400) // 256 = 0400, 32 = 040 - $sql_read_acl = '((fs_mode & 4)=4 OR (fs_mode & 256)=256 AND fs_uid='.(int)Vfs::$user. + $sql_read_acl[$user] = '((fs_mode & 4)=4 OR (fs_mode & 256)=256 AND fs_uid='.$user. ($memberships ? ' OR (fs_mode & 32)=32 AND fs_gid IN('.implode(',',$memberships).')' : '').')'; - //error_log(__METHOD__."() Vfs::\$user=".array2string(Vfs::$user).' --> memberships='.array2string($memberships).' --> '.$sql_read_acl.($memberships?'':': '.function_backtrace())); + error_log(__METHOD__."() user=".array2string($user).' --> memberships='.array2string($memberships).' --> '.$sql_read_acl.($memberships?'':': '.function_backtrace())); } - return $sql_read_acl; + return $sql_read_acl[$user]; } /** @@ -1341,10 +1348,9 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface * @param string $path * @return string|boolean content of the symlink or false if $url is no symlink (or not found) */ - static function readlink($path) + function readlink($path) { - $vfs = new self(); - $link = !($lstat = $vfs->url_stat($path,STREAM_URL_STAT_LINK)) || is_null($lstat['readlink']) ? false : $lstat['readlink']; + $link = !($lstat = $this->url_stat($path,STREAM_URL_STAT_LINK)) || is_null($lstat['readlink']) ? false : $lstat['readlink']; if (self::LOG_LEVEL > 1) error_log(__METHOD__."('$path') = $link"); @@ -1358,18 +1364,17 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface * @param string $link * @return boolean true on success false on error */ - static function symlink($target,$link) + function symlink($target, $link) { if (self::LOG_LEVEL > 1) error_log(__METHOD__."('$target','$link')"); - $inst = new static(); - if ($inst->url_stat($link,0)) + if ($this->url_stat($link,0)) { if (self::LOG_LEVEL > 0) error_log(__METHOD__."('$target','$link') $link exists, returning false!"); return false; // $link already exists } if (!($dir = Vfs::dirname($link)) || - !Vfs::check_access($dir,Vfs::WRITABLE,$dir_stat=$inst->url_stat($dir,0))) + !Vfs::check_access($dir,Vfs::WRITABLE, $dir_stat=$this->url_stat($dir,0), $this->user)) { if (self::LOG_LEVEL > 0) error_log(__METHOD__."('$target','$link') returning false! (!is_writable('$dir'), dir_stat=".array2string($dir_stat).")"); return false; // parent dir does not exist or is not writable @@ -1384,7 +1389,7 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface 'fs_name' => self::limit_filename(Vfs::basename($link)), 'fs_dir' => $dir_stat['ino'], 'fs_mode' => ($dir_stat['mode'] & 0666), - 'fs_uid' => $dir_stat['uid'] ? $dir_stat['uid'] : Vfs::$user, + 'fs_uid' => $dir_stat['uid'] ? $dir_stat['uid'] : $this->user, 'fs_gid' => $dir_stat['gid'], 'fs_created' => self::_pdo_timestamp(time()), 'fs_modified' => self::_pdo_timestamp(time()), @@ -1407,13 +1412,13 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface * @param int $check mode to check: one or more or'ed together of: 4 = read, 2 = write, 1 = executable * @return boolean */ - static function check_extended_acl($url,$check) + function check_extended_acl($url,$check) { $url_path = Vfs::parse_url($url,PHP_URL_PATH); if (is_null(self::$extended_acl)) { - self::_read_extended_acl(); + $this->_read_extended_acl(); } $access = false; foreach(self::$extended_acl as $path => $rights) @@ -1432,14 +1437,14 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface * Read the extended acl via acl::get_grants('sqlfs') * */ - static protected function _read_extended_acl() + protected function _read_extended_acl() { if ((self::$extended_acl = Api\Cache::getSession(self::EACL_APPNAME, 'extended_acl'))) { return; // ext. ACL read from session. } self::$extended_acl = array(); - if (($rights = $GLOBALS['egw']->acl->get_all_location_rights(Vfs::$user,self::EACL_APPNAME))) + if (($rights = $GLOBALS['egw']->acl->get_all_location_rights($this->user, self::EACL_APPNAME))) { $pathes = self::id2path(array_keys($rights)); } @@ -1732,6 +1737,7 @@ GROUP BY A.fs_id'; // eGW addition to return some extra values 'mime' => $info['fs_mime'], 'readlink' => $info['fs_link'], + 'effectiv-uid' => $info['effectiv-uid'], ); if (self::LOG_LEVEL > 1) error_log(__METHOD__."($info[name]) = ".array2string($stat)); return $stat; @@ -1843,14 +1849,12 @@ GROUP BY A.fs_id'; * @param array $props array of array with values for keys 'name', 'ns', 'val' (null to delete the prop) * @return boolean true if props are updated, false otherwise (eg. ressource not found) */ - static function proppatch($path,array $props) + function proppatch($path,array $props) { - static $inst = null; if (self::LOG_LEVEL > 1) error_log(__METHOD__."(".array2string($path).','.array2string($props)); if (!is_numeric($path)) { - if (!isset($inst)) $inst = new self(); - if (!($stat = $inst->url_stat($path,0))) + if (!($stat = $this->url_stat($path,0))) { return false; } @@ -1910,17 +1914,14 @@ GROUP BY A.fs_id'; * @return array|boolean false on error ($path_ids does not exist), array with props (values for keys 'name', 'ns', 'value'), or * fs_id/path => array of props for $depth==1 or is_array($path_ids) */ - static function propfind($path_ids,$ns=Vfs::DEFAULT_PROP_NAMESPACE) + function propfind($path_ids,$ns=Vfs::DEFAULT_PROP_NAMESPACE) { - static $inst = null; - $ids = is_array($path_ids) ? $path_ids : array($path_ids); foreach($ids as &$id) { if (!is_numeric($id)) { - if (!isset($inst)) $inst = new self(); - if (!($stat = $inst->url_stat($id,0))) + if (!($stat = $this->url_stat($id,0))) { if (self::LOG_LEVEL) error_log(__METHOD__."(".array2string($path_ids).",$ns) path '$id' not found!"); return false; @@ -1974,7 +1975,7 @@ GROUP BY A.fs_id'; */ public static function register() { - stream_register_wrapper(self::SCHEME, __CLASS__); + stream_wrapper_register(self::SCHEME, __CLASS__); } } diff --git a/api/src/Vfs/StreamWrapper.php b/api/src/Vfs/StreamWrapper.php index 67ce97bdfe..52ea461ba5 100644 --- a/api/src/Vfs/StreamWrapper.php +++ b/api/src/Vfs/StreamWrapper.php @@ -7,8 +7,7 @@ * @package api * @subpackage vfs * @author Ralf Becker - * @copyright (c) 2008-16 by Ralf Becker - * @version $Id$ + * @copyright (c) 2008-20 by Ralf Becker */ namespace EGroupware\Api\Vfs; @@ -26,6 +25,8 @@ use EGroupware\Api; */ class StreamWrapper implements StreamWrapperIface { + use UserContext; + /** * Scheme / protocol used for this stream-wrapper */ @@ -39,12 +40,6 @@ class StreamWrapper implements StreamWrapperIface */ const HIDE_UNREADABLES = true; - /** - * optional context param when opening the stream, null if no context passed - * - * @var mixed - */ - var $context; /** * mode-bits, which have to be set for links */ @@ -174,7 +169,12 @@ class StreamWrapper implements StreamWrapperIface // if the url resolves to a symlink to the vfs, resolve this vfs:// url direct if ($url && Vfs::parse_url($url,PHP_URL_SCHEME) == self::SCHEME) { + $user = Vfs::parse_url($url,PHP_URL_USER); $url = self::resolve_url(Vfs::parse_url($url,PHP_URL_PATH)); + if (!empty($user) && empty(parse_url($url, PHP_URL_USER))) + { + $url = str_replace('://', '://'.$user.'@', $url); + } } if (self::LOG_LEVEL > 1) error_log(__METHOD__."($path,file_exists=$file_exists,resolve_last_symlink=$resolve_last_symlink) = '$url'$log"); return $url; @@ -304,6 +304,8 @@ class StreamWrapper implements StreamWrapperIface unset($options,$opened_path); // not used but required by function signature $this->opened_stream = null; + error_log(__METHOD__."('$path', $mode, $options) context=".json_encode(stream_context_get_options($this->context))); + $stat = null; if (!($url = $this->resolve_url_symlinks($path,$mode[0]=='r',true,$stat))) { @@ -313,8 +315,10 @@ class StreamWrapper implements StreamWrapperIface { return false; } - if (!($this->opened_stream = $this->context ? - fopen($url, $mode, false, $this->context) : fopen($url, $mode, false))) + $this->check_set_context($url, true); + + if (!($this->opened_stream = $context ? + fopen($url, $mode, false, $context) : fopen($url, $mode, false))) { return false; } @@ -325,7 +329,7 @@ class StreamWrapper implements StreamWrapperIface // are we requested to treat the opened file as new file (only for files opened NOT for reading) if ($mode[0] != 'r' && !$this->opened_stream_is_new && $this->context && - ($opts = stream_context_get_params($this->context)) && + ($opts = stream_context_get_options($this->context)) && $opts['options'][self::SCHEME]['treat_as_new']) { $this->opened_stream_is_new = true; @@ -538,10 +542,12 @@ class StreamWrapper implements StreamWrapperIface { return false; } + // set user-context + $this->check_set_context($url, true); $stat = $this->url_stat($path, STREAM_URL_STAT_LINK); self::symlinkCache_remove($path); - $ok = unlink($url); + $ok = unlink($url, $this->context); // call "vfs_unlink" hook only after successful unlink, with data from (not longer possible) stat call if ($ok && !class_exists('setup_process', false)) @@ -585,13 +591,16 @@ class StreamWrapper implements StreamWrapperIface { return false; } + // set user-context + $this->check_set_context($url_from); + // if file is moved from one filesystem / wrapper to an other --> copy it (rename fails cross wrappers) if (Vfs::parse_url($url_from,PHP_URL_SCHEME) == Vfs::parse_url($url_to,PHP_URL_SCHEME)) { self::symlinkCache_remove($path_from); - $ret = rename($url_from,$url_to); + $ret = rename($url_from, $url_to, $this->context); } - elseif (($from = fopen($url_from,'r')) && ($to = fopen($url_to,'w'))) + elseif (($from = fopen($url_from,'r', false, $this->context)) && ($to = fopen($url_to,'w'))) { $ret = stream_copy_to_stream($from,$to) !== false; fclose($from); @@ -642,6 +651,11 @@ class StreamWrapper implements StreamWrapperIface { return false; } + // set user context + if (Vfs::parse_url($url, PHP_URL_USER)) + { + $this->check_set_context($url, true); + } // check if recursive option is set and needed if (($options & STREAM_MKDIR_RECURSIVE) && ($parent_url = Vfs::dirname($url)) && @@ -660,7 +674,7 @@ class StreamWrapper implements StreamWrapperIface $options &= ~STREAM_MKDIR_RECURSIVE; } - $ret = mkdir($url,$mode,$options); + $ret = mkdir($url, $mode, $options, $this->context); // call "vfs_mkdir" hook if ($ret && !class_exists('setup_process', false)) @@ -702,8 +716,13 @@ class StreamWrapper implements StreamWrapperIface } $stat = $this->url_stat($path, STREAM_URL_STAT_LINK); + // set user context + if (Vfs::parse_url($url, PHP_URL_USER)) + { + $this->check_set_context($url, true); + } self::symlinkCache_remove($path); - $ok = rmdir($url); + $ok = rmdir($url, $this->context); // call "vfs_rmdir" hook, only after successful rmdir if ($ok && !class_exists('setup_process', false)) @@ -735,6 +754,9 @@ class StreamWrapper implements StreamWrapperIface if (self::LOG_LEVEL > 0) error_log(__METHOD__."( $path,$options) resolve_url_symlinks() failed!"); return false; } + // need to set user-context from resolved url + $this->check_set_context($this->opened_dir_url, true); + if (!($this->opened_dir = $this->context ? opendir($this->opened_dir_url, $this->context) : opendir($this->opened_dir_url))) { @@ -790,13 +812,22 @@ class StreamWrapper implements StreamWrapperIface */ function url_stat ( $path, $flags, $try_create_home=false, $check_symlink_components=true, $check_symlink_depth=self::MAX_SYMLINK_DEPTH, $try_reconnect=true ) { + // we have no context, but $path is a URL with a valid user --> set it + $this->check_set_context($path); + if (!($url = self::resolve_url($path,!($flags & STREAM_URL_STAT_LINK), $check_symlink_components))) { if (self::LOG_LEVEL > 0) error_log(__METHOD__."('$path',$flags) can NOT resolve path!"); return false; } + if (empty(parse_url($url, PHP_URL_USER))) + { + $url = str_replace('://', '://'.Api\Accounts::id2name($this->context ? stream_context_get_options($this->context)[self::SCHEME]['user'] : Vfs::$user).'@', $url); + } + try { +clearstatcache(); // testwise, NOT good for performance if ($flags & STREAM_URL_STAT_LINK) { $stat = @lstat($url); // suppressed the stat failed warnings @@ -853,6 +884,7 @@ class StreamWrapper implements StreamWrapperIface // if numer of tries is exceeded, re-throw exception throw $e; } +clearstatcache(); // testwise, NOT good for performance // check if a failed url_stat was for a home dir, in that case silently create it if (!$stat && $try_create_home && Vfs::dirname(Vfs::parse_url($path,PHP_URL_PATH)) == '/home' && ($id = $GLOBALS['egw']->accounts->name2id(Vfs::basename($path))) && @@ -901,6 +933,38 @@ class StreamWrapper implements StreamWrapperIface return $stat;*/ } + /** + * The stream_wrapper interface checks is_{readable|writable|executable} against the webservers uid, + * which is wrong in case of our vfs, as we use the current users id and memberships + * + * @param string $path path + * @param int $check mode to check: one or more or'ed together of: 4 = self::READABLE, + * 2 = self::WRITABLE, 1 = self::EXECUTABLE + * @return boolean + */ + function check_access($path, $check) + { + if (!($stat = $this->url_stat($path, 0))) + { + $ret = false; + } + else + { + if (($account_lid = Vfs::parse_url($stat['url'], PHP_URL_USER)) && + ($account_id = Api\Accounts::getInstance()->name2id($account_lid))) + { + $user = $account_id; + } + else + { + $user = $this->user ?: Vfs::$user; + } + $ret = Vfs::check_access($path, $check, $stat, $user); + } + error_log(__METHOD__."('$path', $check) user=".Api\Accounts::id2name($user).", effective user=$user=".Api\Accounts::id2name($user).", mode=".decoct($stat['mode'] & 0777).", uid=".($stat['uid']?Api\Accounts::id2name($stat['uid']):0).", uid=".($stat['gid']?Api\Accounts::id2name(-$stat['gid']):0)." returning ".array2string($ret)); + return $ret; + } + /** * Check if path (which fails the stat call) contains symlinks in path-components other then the last one * @@ -1156,6 +1220,10 @@ class StreamWrapper implements StreamWrapperIface */ static function scheme2class($scheme) { + if ($scheme === self::SCHEME) + { + return __CLASS__; + } list($app, $app_scheme) = explode('.', $scheme); foreach(array( empty($app_scheme) ? 'EGroupware\\Api\\Vfs\\'.ucfirst($scheme).'\\StreamWrapper' : // streamwrapper in Api\Vfs @@ -1329,12 +1397,19 @@ class StreamWrapper implements StreamWrapperIface if (in_array(self::SCHEME, stream_get_wrappers())) { stream_wrapper_unregister(self::SCHEME); } - stream_register_wrapper(self::SCHEME,__CLASS__); + stream_wrapper_register(self::SCHEME,__CLASS__); if (($fstab = $GLOBALS['egw_info']['server']['vfs_fstab']) && is_array($fstab) && count($fstab)) { self::$fstab = $fstab; } + + // set default context for our schema ('vfs') with current user + if (!($context = stream_context_get_options(stream_context_get_default())) || empty($context[self::SCHEME]['user'])) + { + $context[self::SCHEME]['user'] = (int)$GLOBALS['egw_info']['user']['account_id']; + stream_context_set_default($context); + } } } diff --git a/api/src/Vfs/UserContext.php b/api/src/Vfs/UserContext.php new file mode 100644 index 0000000000..1a01616827 --- /dev/null +++ b/api/src/Vfs/UserContext.php @@ -0,0 +1,119 @@ + + * @copyright (c) 2020 by Ralf Becker + */ + +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; + } +} diff --git a/vfs-context.php b/vfs-context.php new file mode 100644 index 0000000000..b0ce8fe5db --- /dev/null +++ b/vfs-context.php @@ -0,0 +1,43 @@ + [ + 'currentapp' => 'login', + ], +]; +require_once __DIR__.'/header-default.inc.php'; + +$GLOBALS['egw_info']['user'] = [ + 'account_id' => 5, + 'account_lid' => 'ralf', +]; + +var_dump(Vfs\StreamWrapper::mount()); + +var_dump(file_put_contents('vfs://default/home/ralf/test.txt', "Just a test ;)\n")); + +var_dump($f=fopen('vfs://default/home/ralf/test.txt', 'r'), fread($f, 100), fclose($f)); + +Vfs::$is_root = true; +var_dump(file_put_contents('vfs://default/home/birgit/test.txt', "Just a test ;)\n")); +var_dump(Vfs\StreamWrapper::mount('vfs://birgit@default/home/birgit', '/home/ralf/birgit')); +Vfs::$is_root = false; + +var_dump(Vfs\StreamWrapper::mount()); + +var_dump("Vfs::resolve_url('/home/ralf/birgit/test.txt')=".Vfs::resolve_url('/home/ralf/birgit/test.txt')); +var_dump("Vfs::url_stat('/home/ralf/birgit/test.txt')=".json_encode(Vfs::stat('/home/ralf/birgit/test.txt'), JSON_UNESCAPED_SLASHES)); +var_dump("Vfs::is_readable('/home/ralf/birgit/test.txt')=".json_encode(Vfs::is_readable('/home/ralf/birgit/test.txt'))); +var_dump("fopen('vfs://default/home/ralf/birgit/test.txt', 'r')", $f=fopen('vfs://default/home/ralf/birgit/test.txt', 'r'), fread($f, 100), fclose($f)); + +var_dump("Vfs::url_stat('/home/ralf/birgit/test.txt')=".json_encode(Vfs::stat('/home/ralf/birgit/test-dir'))); +var_dump("Vfs::mkdir('/home/ralf/birgit/test-dir')=".json_encode(Vfs::mkdir('/home/ralf/birgit/test-dir'))); +var_dump("Vfs::url_stat('/home/ralf/birgit/test.txt')=".json_encode(Vfs::stat('/home/ralf/birgit/test-dir'), JSON_UNESCAPED_SLASHES)); +var_dump("Vfs::rmdir('/home/ralf/birgit/test-dir')=".json_encode(Vfs::rmdir('/home/ralf/birgit/test-dir'))); +var_dump("Vfs::url_stat('/home/ralf/birgit/test.txt')=".json_encode(Vfs::stat('/home/ralf/birgit/test-dir'))); + +var_dump("Vfs::scandir('/home/ralf/birgit')=".json_encode(Vfs::scandir('/home/ralf/birgit'), JSON_UNESCAPED_SLASHES)); +var_dump("Vfs::remove('/home/ralf/birgit/test.txt')=".json_encode(Vfs::remove('/home/ralf/birgit/test.txt'))); +var_dump("Vfs::scandir('/home/ralf/birgit')=".json_encode(Vfs::scandir('/home/ralf/birgit'), JSON_UNESCAPED_SLASHES));