* @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 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; } else { if (!isset($this->context)) // PHP set's it before constructor is called! { $this->context = stream_context_get_default(); } // if context set by PHP contains no user, set user from our default context (Vfs::$user) elseif (empty(stream_context_get_options($this->context)[Vfs::SCHEME]['user'])) { stream_context_set_option($this->context, stream_context_get_options(stream_context_get_default())); } if (is_string($url_or_context)) { $this->check_set_context($url_or_context); } } } /** * Check if we have an url with a user --> set it as context * * @param $url */ protected function check_set_context($url) { if ($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 Api\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 } // only vfs stream-wrapper sets $stat['url'], use given url instead if (!isset($stat['url']) && $path[0] !== '/') { $stat['url'] = $path; } // 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 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; } } // 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'] : Vfs::$user; } 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); } } else { $this->context = stream_context_create(); } break; } } /** * Get user context for given url, eg. to use with regular stream functions * * @param string $url * @param array $extra =[] addtional context options * @return resource context with user, plus optional extra options */ public static function userContext($url, array $extra=[]) { if (($user = Vfs::parse_url($url, PHP_URL_USER)) && ($account_id = Api\Accounts::getInstance()->name2id($user)) && ($account_id != Vfs::$user) || $extra) // never set extra options on default context! { $context = stream_context_create(array_merge_recursive([Vfs::SCHEME => ['user' => (int)$account_id ?: Vfs::$user]], $extra)); } else { $context = stream_context_get_default(); } return $context; } /** * @param string $name * @return bool */ public function __isset($name) { return $this->__get($name) !== null; } }