Issues (4868)

api/src/Vfs/Sharing.php (2 issues)

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
The type EGroupware\Collabora\Wopi was not found. Maybe you did not declare it correctly or list all dependencies?

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:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
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
Consider adding parentheses for clarity. Current Interpretation: (is_dir($path) && ! EGro...fs::PREFIX . $tmp_file), Probably Intended Meaning: is_dir($path) && (! EGro...s::PREFIX . $tmp_file))
Loading history...
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
}