EGroupware /
egroupware
| 1 | <?php |
||
| 2 | /** |
||
| 3 | * EGroupware API: VFS sharing |
||
| 4 | * |
||
| 5 | * @link http://www.egroupware.org |
||
| 6 | * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License |
||
| 7 | * @package api |
||
| 8 | * @subpackage Vfs |
||
| 9 | * @author Ralf Becker <[email protected]> |
||
| 10 | * @copyright (c) 2014-16 by Ralf Becker <[email protected]> |
||
| 11 | * @version $Id$ |
||
| 12 | */ |
||
| 13 | |||
| 14 | namespace EGroupware\Api\Vfs; |
||
| 15 | |||
| 16 | use EGroupware\Api; |
||
| 17 | use EGroupware\Api\Vfs; |
||
| 18 | use EGroupware\Collabora\Wopi; |
||
|
0 ignored issues
–
show
|
|||
| 19 | |||
| 20 | use filemanager_ui; |
||
| 21 | |||
| 22 | /** |
||
| 23 | * VFS sharing |
||
| 24 | * |
||
| 25 | * Token generation uses openssl_random_pseudo_bytes, if available, otherwise |
||
| 26 | * mt_rand based Api\Auth::randomstring is used. |
||
| 27 | * |
||
| 28 | * Existing user sessions are kept whenever possible by an additional mount into regular VFS: |
||
| 29 | * - share owner is current user (no problems with rights, they simply match) |
||
| 30 | * - share owner has owner-right for share: we create a temp. eACL for current user |
||
| 31 | * --> in all other cases session will be replaced with one of the anonymous user, |
||
| 32 | * as we dont support mounting with rights of share owner (VFS uses Vfs::$user!) |
||
| 33 | * |
||
| 34 | * @todo handle mounts of an entry directory /apps/$app/$id |
||
| 35 | * @todo handle mounts inside shared directory (they get currently lost) |
||
| 36 | * @todo handle absolute symlinks (wont work as we use share as root) |
||
| 37 | */ |
||
| 38 | class Sharing extends \EGroupware\Api\Sharing |
||
| 39 | { |
||
| 40 | |||
| 41 | /** |
||
| 42 | * Modes ATTACH is NOT a sharing mode, but it is traditional mode in email |
||
| 43 | */ |
||
| 44 | const ATTACH = 'attach'; |
||
| 45 | const LINK = 'link'; |
||
| 46 | const READONLY = 'share_ro'; |
||
| 47 | const WRITABLE = 'share_rw'; |
||
| 48 | |||
| 49 | /** |
||
| 50 | * Modes for sharing files |
||
| 51 | * |
||
| 52 | * @var array |
||
| 53 | */ |
||
| 54 | static $modes = array( |
||
| 55 | self::ATTACH => array( |
||
| 56 | 'label' => 'Attachment', |
||
| 57 | 'title' => 'Works reliable for total size up to 1-2 MB, might work for 5-10 MB, most likely to fail for >10MB', |
||
| 58 | ), |
||
| 59 | self::LINK => array( |
||
| 60 | 'label' => 'Download link', |
||
| 61 | 'title' => 'Link is appended to mail allowing recipients to download currently attached version of files', |
||
| 62 | ), |
||
| 63 | self::READONLY => array( |
||
| 64 | 'label' => 'Readonly share', |
||
| 65 | 'title' => 'Link is appended to mail allowing recipients to download up to date version of files', |
||
| 66 | ), |
||
| 67 | self::WRITABLE => array( |
||
| 68 | 'label' => 'Writable share', |
||
| 69 | 'title' => 'Link is appended to mail allowing recipients to download or modify up to date version of files (EPL only)' |
||
| 70 | ), |
||
| 71 | ); |
||
| 72 | |||
| 73 | /** |
||
| 74 | * Create sharing session |
||
| 75 | * |
||
| 76 | * Certain cases: |
||
| 77 | * a) there is not session $keep_session === null |
||
| 78 | * --> create new anon session with just filemanager rights and share as fstab |
||
| 79 | * b) there is a session $keep_session === true |
||
| 80 | * b1) current user is share owner (eg. checking the link) |
||
| 81 | * --> mount share under token additionally |
||
| 82 | * b2) current user not share owner |
||
| 83 | * b2a) need/use filemanager UI (eg. directory) |
||
| 84 | * --> destroy current session and continue with a) |
||
| 85 | * b2b) single file or WebDAV |
||
| 86 | * --> modify EGroupware enviroment for that request only, no change in session |
||
| 87 | * |
||
| 88 | * @param boolean $keep_session =null null: create a new session, true: try mounting it into existing (already verified) session |
||
| 89 | * @return string with sessionid, does NOT return if no session created |
||
| 90 | */ |
||
| 91 | public static function setup_share($keep_session, &$share) |
||
| 92 | { |
||
| 93 | |||
| 94 | // need to reset fs_tab, as resolve_url does NOT work with just share mounted |
||
| 95 | if (count($GLOBALS['egw_info']['server']['vfs_fstab']) <= 1) |
||
| 96 | { |
||
| 97 | unset($GLOBALS['egw_info']['server']['vfs_fstab']); // triggers reset of fstab in mount() |
||
| 98 | $GLOBALS['egw_info']['server']['vfs_fstab'] = Vfs::mount(); |
||
| 99 | Vfs::clearstatcache(); |
||
| 100 | } |
||
| 101 | $share['resolve_url'] = Vfs::resolve_url($share['share_path'], true, true, true, true); // true = fix evtl. contained url parameter |
||
| 102 | // if share not writable append ro=1 to mount url to make it readonly |
||
| 103 | if (!($share['share_writable'] & 1)) |
||
| 104 | { |
||
| 105 | $share['resolve_url'] .= (strpos($share['resolve_url'], '?') ? '&' : '?').'ro=1'; |
||
| 106 | } |
||
| 107 | //_debug_array($share); |
||
| 108 | |||
| 109 | if ($keep_session) // add share to existing session |
||
| 110 | { |
||
| 111 | $share['share_root'] = '/'.$share['share_token']; |
||
| 112 | |||
| 113 | // if current user is not the share owner, we cant just mount share |
||
| 114 | if (Vfs::$user != $share['share_owner']) |
||
| 115 | { |
||
| 116 | $keep_session = false; |
||
| 117 | } |
||
| 118 | } |
||
| 119 | if (!$keep_session) // do NOT change to else, as we might have set $keep_session=false! |
||
| 120 | { |
||
| 121 | // only allow filemanager app & collabora |
||
| 122 | // (In some cases, $GLOBALS['egw_info']['apps'] is not yet set) |
||
| 123 | $apps = $GLOBALS['egw']->acl->get_user_applications($share['share_owner']); |
||
| 124 | $GLOBALS['egw_info']['user']['apps'] = array( |
||
| 125 | 'filemanager' => $GLOBALS['egw_info']['apps']['filemanager'] || true, |
||
| 126 | 'collabora' => $GLOBALS['egw_info']['apps']['collabora'] || $apps['collabora'] |
||
| 127 | ); |
||
| 128 | |||
| 129 | $share['share_root'] = '/'; |
||
| 130 | Vfs::$user = $share['share_owner']; |
||
| 131 | |||
| 132 | // Need to re-init stream wrapper, as some of them look at |
||
| 133 | // preferences or permissions |
||
| 134 | $scheme = Vfs\StreamWrapper::scheme2class(Vfs::parse_url($share['resolve_url'],PHP_URL_SCHEME)); |
||
| 135 | if($scheme && method_exists($scheme, 'init_static')) |
||
| 136 | { |
||
| 137 | $scheme::init_static(); |
||
| 138 | } |
||
| 139 | } |
||
| 140 | |||
| 141 | // mounting share |
||
| 142 | Vfs::$is_root = true; |
||
| 143 | if (!Vfs::mount($share['resolve_url'], $share['share_root'], false, false, !$keep_session)) |
||
| 144 | { |
||
| 145 | sleep(1); |
||
| 146 | return static::share_fail( |
||
| 147 | '404 Not Found', |
||
| 148 | "Requested resource '/".htmlspecialchars($share['share_token'])."' does NOT exist!\n" |
||
| 149 | ); |
||
| 150 | } |
||
| 151 | Vfs::$is_root = false; |
||
| 152 | Vfs::clearstatcache(); |
||
| 153 | // clear link-cache and load link registry without permission check to access /apps |
||
| 154 | Api\Link::init_static(true); |
||
| 155 | } |
||
| 156 | |||
| 157 | protected function after_login() |
||
| 158 | { |
||
| 159 | // only allow filemanager app (gets overwritten by session::create) |
||
| 160 | $GLOBALS['egw_info']['user']['apps'] = array( |
||
| 161 | 'filemanager' => $GLOBALS['egw_info']['apps']['filemanager'] |
||
| 162 | ); |
||
| 163 | // check if sharee has Collabora run rights --> give is to share too |
||
| 164 | $apps = $GLOBALS['egw']->acl->get_user_applications($this->share['share_owner']); |
||
| 165 | if (!empty($apps['collabora'])) |
||
| 166 | { |
||
| 167 | $GLOBALS['egw_info']['user']['apps']['collabora'] = $GLOBALS['egw_info']['apps']['collabora']; |
||
| 168 | } |
||
| 169 | } |
||
| 170 | |||
| 171 | /** |
||
| 172 | * Server a request on a share specified in REQUEST_URI |
||
| 173 | */ |
||
| 174 | public function get_ui() |
||
| 175 | { |
||
| 176 | // run full eTemplate2 UI for directories |
||
| 177 | $_GET['path'] = $this->share['share_root']; |
||
| 178 | $GLOBALS['egw_info']['user']['preferences']['filemanager']['nm_view'] = 'tile'; |
||
| 179 | $_GET['cd'] = 'no'; |
||
| 180 | $GLOBALS['egw_info']['flags']['js_link_registry'] = true; |
||
| 181 | $GLOBALS['egw_info']['flags']['currentapp'] = 'filemanager'; |
||
| 182 | Api\Framework::includeCSS('filemanager', 'sharing'); |
||
| 183 | $ui = new SharingUi(); |
||
| 184 | $ui->index(); |
||
| 185 | } |
||
| 186 | |||
| 187 | /** |
||
| 188 | * Create a new share |
||
| 189 | * |
||
| 190 | * @param string $path either path in temp_dir or vfs with optional vfs scheme |
||
| 191 | * @param string $mode self::LINK: copy file in users tmp-dir or self::READABLE share given vfs file, |
||
| 192 | * if no vfs behave as self::LINK |
||
| 193 | * @param string $name filename to use for $mode==self::LINK, default basename of $path |
||
| 194 | * @param string|array $recipients one or more recipient email addresses |
||
| 195 | * @param array $extra =array() extra data to store |
||
| 196 | * @throw Api\Exception\NotFound if $path not found |
||
| 197 | * @throw Api\Exception\AssertionFailed if user temp. directory does not exist and can not be created |
||
| 198 | * @return array with share data, eg. value for key 'share_token' |
||
| 199 | */ |
||
| 200 | public static function create($path, $mode, $name, $recipients, $extra=array()) |
||
| 201 | { |
||
| 202 | if (!isset(self::$db)) self::$db = $GLOBALS['egw']->db; |
||
| 203 | |||
| 204 | // Parent puts the application as a prefix. If we're coming from there, pull it off |
||
| 205 | if(strpos($path, 'filemanager::') === 0) |
||
| 206 | { |
||
| 207 | list(,$path) = explode('::', $path); |
||
| 208 | } |
||
| 209 | if (empty($name)) $name = $path; |
||
| 210 | |||
| 211 | $path2tmp =& Api\Cache::getSession(__CLASS__, 'path2tmp'); |
||
| 212 | |||
| 213 | // allow filesystem path only for temp_dir |
||
| 214 | $temp_dir = $GLOBALS['egw_info']['server']['temp_dir'].'/'; |
||
| 215 | if (substr($path, 0, strlen($temp_dir)) == $temp_dir) |
||
| 216 | { |
||
| 217 | $mode = self::LINK; |
||
| 218 | $exists = file_exists($path) && is_readable($path); |
||
| 219 | } |
||
| 220 | else |
||
| 221 | { |
||
| 222 | if(parse_url($path, PHP_URL_SCHEME) !== 'vfs') |
||
| 223 | { |
||
| 224 | $path = 'vfs://default'.($path[0] == '/' ? '' : '/').$path; |
||
| 225 | } |
||
| 226 | |||
| 227 | // We don't allow sharing links, share target instead |
||
| 228 | if(($target = Vfs::readlink($path))) |
||
| 229 | { |
||
| 230 | $path = $target; |
||
| 231 | } |
||
| 232 | |||
| 233 | if (($exists = ($stat = Vfs::stat($path)) && Vfs::check_access($path, Vfs::READABLE, $stat))) |
||
| 234 | { |
||
| 235 | // Make sure we get the correct path if sharing from a share |
||
| 236 | if(isset($GLOBALS['egw']->sharing) && $exists) |
||
| 237 | { |
||
| 238 | $resolved_stat = Vfs::parse_url($stat['url']); |
||
| 239 | $path = 'vfs://default'. $resolved_stat['path']; |
||
| 240 | } |
||
| 241 | |||
| 242 | $vfs_path = $path; |
||
| 243 | } |
||
| 244 | } |
||
| 245 | // check if file exists and is readable |
||
| 246 | if (!$exists) |
||
| 247 | { |
||
| 248 | throw new Api\Exception\NotFound("'$path' NOT found!"); |
||
| 249 | } |
||
| 250 | // check if file has been shared before, with identical attributes |
||
| 251 | if (($mode != self::LINK )) |
||
| 252 | { |
||
| 253 | return parent::create($vfs_path ? $vfs_path : $path, $mode, $name, $recipients, $extra); |
||
| 254 | } |
||
| 255 | else |
||
| 256 | { |
||
| 257 | // if not create new share |
||
| 258 | if ($mode == 'link') |
||
| 259 | { |
||
| 260 | $user_tmp = '/home/'.$GLOBALS['egw_info']['user']['account_lid'].'/.tmp'; |
||
| 261 | if (!Vfs::file_exists($user_tmp) && !Vfs::mkdir($user_tmp, null, STREAM_MKDIR_RECURSIVE)) |
||
| 262 | { |
||
| 263 | throw new Api\Exception\AssertionFailed("Could NOT create temp. directory '$user_tmp'!"); |
||
| 264 | } |
||
| 265 | $n = 0; |
||
| 266 | do { |
||
| 267 | $tmp_file = Vfs::concat($user_tmp, ($n?$n.'.':'').Vfs::basename($name)); |
||
| 268 | } |
||
| 269 | while(!(is_dir($path) && Vfs::mkdir($tmp_file, null, STREAM_MKDIR_RECURSIVE) || |
||
| 270 | !is_dir($path) && (!Vfs::file_exists($tmp_file) && ($fp = Vfs::fopen($tmp_file, 'x')) || |
||
| 271 | // do not copy identical files again to users tmp dir, just re-use them |
||
| 272 | Vfs::file_exists($tmp_file) && Vfs::compare(Vfs::PREFIX.$tmp_file, $path))) && $n++ < 100); |
||
| 273 | |||
| 274 | if ($n >= 100) |
||
| 275 | { |
||
| 276 | throw new Api\Exception\AssertionFailed("Could NOT create temp. file '$tmp_file'!"); |
||
| 277 | } |
||
| 278 | if ($fp) fclose($fp); |
||
| 279 | |||
| 280 | if (is_dir($path) && !Vfs::copy_files(array($path), $tmp_file) || |
||
|
0 ignored issues
–
show
|
|||
| 281 | !is_dir($path) && !copy($path, Vfs::PREFIX.$tmp_file)) |
||
| 282 | { |
||
| 283 | throw new Api\Exception\AssertionFailed("Could NOT create temp. file '$tmp_file'!"); |
||
| 284 | } |
||
| 285 | // store temp. path in session, to be able to add more recipients |
||
| 286 | $path2tmp[$path] = $tmp_file; |
||
| 287 | |||
| 288 | $vfs_path = $tmp_file; |
||
| 289 | } |
||
| 290 | |||
| 291 | return parent::create($vfs_path, $mode, $name, $recipients, $extra); |
||
| 292 | } |
||
| 293 | } |
||
| 294 | |||
| 295 | /** |
||
| 296 | * Delete specified shares and unlink temp. files |
||
| 297 | * |
||
| 298 | * @param int|array $keys |
||
| 299 | * @return int number of deleted shares |
||
| 300 | */ |
||
| 301 | public static function delete($keys) |
||
| 302 | { |
||
| 303 | self::$db = $GLOBALS['egw']->db; |
||
| 304 | |||
| 305 | if (is_scalar($keys)) $keys = array('share_id' => $keys); |
||
| 306 | |||
| 307 | // get all temp. files, to be able to delete them |
||
| 308 | $tmp_paths = array(); |
||
| 309 | foreach(self::$db->select(self::TABLE, 'share_path', array( |
||
| 310 | "share_path LIKE '/home/%/.tmp/%'")+$keys, __LINE__, __FILE__, false) as $row) |
||
| 311 | { |
||
| 312 | $tmp_paths[] = $row['share_path']; |
||
| 313 | } |
||
| 314 | |||
| 315 | $deleted = parent::delete($keys); |
||
| 316 | |||
| 317 | // check if temp. files are used elsewhere |
||
| 318 | if ($tmp_paths) |
||
| 319 | { |
||
| 320 | foreach(self::$db->select(self::TABLE, 'share_path,COUNT(*) AS cnt', array( |
||
| 321 | 'share_path' => $tmp_paths, |
||
| 322 | ), __LINE__, __FILE__, false, 'GROUP BY share_path') as $row) |
||
| 323 | { |
||
| 324 | if (($key = array_search($row['share_path'], $tmp_paths))) |
||
| 325 | { |
||
| 326 | unset($tmp_paths[$key]); |
||
| 327 | } |
||
| 328 | } |
||
| 329 | // if not delete them |
||
| 330 | foreach($tmp_paths as $path) |
||
| 331 | { |
||
| 332 | Vfs::remove($path); |
||
| 333 | } |
||
| 334 | } |
||
| 335 | return $deleted; |
||
| 336 | } |
||
| 337 | |||
| 338 | /** |
||
| 339 | * Check that a share path still exists (and is readable) |
||
| 340 | */ |
||
| 341 | protected static function check_path($share) |
||
| 342 | { |
||
| 343 | return file_exists($share['share_path']); |
||
| 344 | } |
||
| 345 | |||
| 346 | /** |
||
| 347 | * Get actions for sharing an entry from filemanager |
||
| 348 | * |
||
| 349 | * @param string $appname |
||
| 350 | * @param int $group Current menu group |
||
| 351 | */ |
||
| 352 | public static function get_actions($appname, $group = 6) |
||
| 353 | { |
||
| 354 | $actions = parent::get_actions('filemanager', $group); |
||
| 355 | |||
| 356 | // This one makes no sense for filemanager |
||
| 357 | unset($actions['share']['children']['shareFiles']); |
||
| 358 | |||
| 359 | // Move these around to mesh nicely if collabora is available |
||
| 360 | $actions['share']['children']['shareReadonlyLink']['group'] = 2; |
||
| 361 | $actions['share']['children']['shareReadonlyLink']['order'] = 22; |
||
| 362 | $actions['share']['children']['shareWritable']['group'] = 3; |
||
| 363 | |||
| 364 | // Add in merge to document |
||
| 365 | if (class_exists($appname.'_merge')) |
||
| 366 | { |
||
| 367 | $documents = call_user_func(array($appname.'_merge', 'document_action'), |
||
| 368 | $GLOBALS['egw_info']['user']['preferences'][$appname]['document_dir'], |
||
| 369 | 2, 'Insert in document', 'shareDocument_' |
||
| 370 | ); |
||
| 371 | $documents['order'] = 25; |
||
| 372 | |||
| 373 | // Mail only |
||
| 374 | if ($documents['children']['message/rfc822']) |
||
| 375 | { |
||
| 376 | // Just email already filtered out |
||
| 377 | $documents['children'] = $documents['children']['message/rfc822']['children']; |
||
| 378 | } |
||
| 379 | foreach($documents['children'] as $key => &$document) |
||
| 380 | { |
||
| 381 | if(strpos($document['target'],'compose_') === FALSE) |
||
| 382 | { |
||
| 383 | unset($documents['children'][$key]); |
||
| 384 | continue; |
||
| 385 | } |
||
| 386 | |||
| 387 | $document['allowOnMultiple'] = true; |
||
| 388 | $document['onExecute'] = "javaScript:app.$appname.share_merge"; |
||
| 389 | } |
||
| 390 | $documents['enabled'] = $documents['enabled'] && (boolean)$documents['children'] && !!($GLOBALS['egw_info']['user']['apps']['stylite']); |
||
| 391 | $actions['share']['children']['shareDocuments'] = $documents; |
||
| 392 | } |
||
| 393 | |||
| 394 | return $actions; |
||
| 395 | } |
||
| 396 | |||
| 397 | } |
||
| 398 | |||
| 399 | if (file_exists(__DIR__.'/../../../filemanager/inc/class.filemanager_ui.inc.php')) |
||
| 400 | { |
||
| 401 | require_once __DIR__.'/../../../filemanager/inc/class.filemanager_ui.inc.php'; |
||
| 402 | |||
| 403 | class SharingUi extends filemanager_ui |
||
| 404 | { |
||
| 405 | /** |
||
| 406 | * Get the configured start directory for the current user |
||
| 407 | * |
||
| 408 | * @return string |
||
| 409 | */ |
||
| 410 | static function get_home_dir() |
||
| 411 | { |
||
| 412 | return $GLOBALS['egw']->sharing->get_root(); |
||
| 413 | } |
||
| 414 | |||
| 415 | /** |
||
| 416 | * Context menu |
||
| 417 | * |
||
| 418 | * @return array |
||
| 419 | */ |
||
| 420 | public static function get_actions() |
||
| 421 | { |
||
| 422 | $actions = parent::get_actions(); |
||
| 423 | $group = 1; |
||
| 424 | // do not add edit setting action when we are in sharing |
||
| 425 | unset($actions['edit']); |
||
| 426 | if(Vfs::is_writable($GLOBALS['egw']->sharing->get_root())) |
||
| 427 | { |
||
| 428 | return $actions; |
||
| 429 | } |
||
| 430 | $actions+= array( |
||
| 431 | 'egw_copy' => array( |
||
| 432 | 'enabled' => false, |
||
| 433 | 'group' => $group + 0.5, |
||
| 434 | 'hideOnDisabled' => true |
||
| 435 | ), |
||
| 436 | 'egw_copy_add' => array( |
||
| 437 | 'enabled' => false, |
||
| 438 | 'group' => $group + 0.5, |
||
| 439 | 'hideOnDisabled' => true |
||
| 440 | ), |
||
| 441 | 'paste' => array( |
||
| 442 | 'enabled' => false, |
||
| 443 | 'group' => $group + 0.5, |
||
| 444 | 'hideOnDisabled' => true |
||
| 445 | ), |
||
| 446 | ); |
||
| 447 | return $actions; |
||
| 448 | } |
||
| 449 | |||
| 450 | protected function get_vfs_options($query) |
||
| 451 | { |
||
| 452 | $options = parent::get_vfs_options($query); |
||
| 453 | |||
| 454 | // Hide symlinks |
||
| 455 | $options['type'] = '!l'; |
||
| 456 | |||
| 457 | return $options; |
||
| 458 | } |
||
| 459 | } |
||
| 460 | } |
The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g.
excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths