1<?php
2
3/**
4 * EGroupware Sharing base class
5 *
6 * @link http://www.egroupware.org
7 * @author Ralf Becker <rb@stylite.de>
8 * @copyright (c) 2014-16 by Ralf Becker <rb@stylite.de>
9 * @package api
10 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
11 */
12
13namespace EGroupware\Api;
14
15use EGroupware\Api\Vfs\HiddenUploadSharing;
16
17/**
18 * VFS sharing
19 *
20 * Token generation uses openssl_random_pseudo_bytes, if available, otherwise
21 * mt_rand based Api\Auth::randomstring is used.
22 *
23 * Existing user sessions are kept whenever possible by an additional mount into regular VFS:
24 * - share owner is current user (no problems with rights, they simply match)
25 * - share owner has owner-right for share: we create a temp. eACL for current user
26 * --> in all other cases session will be replaced with one of the anonymous user,
27 *     as we dont support mounting with rights of share owner (VFS uses Vfs::$user!)
28 *
29 * @todo handle mounts of an entry directory /apps/$app/$id
30 * @todo handle mounts inside shared directory (they get currently lost)
31 * @todo handle absolute symlinks (wont work as we use share as root)
32 */
33class Sharing
34{
35	/**
36	 * Length of base64 encoded token (real length is only 3/4 of it)
37	 *
38	 * Dropbox uses just 15 chars (letters/numbers 5-6 bit), php sessions use 32 chars (hex = 4bits),
39	 * so 32 chars of base64 = 6bits should be plenty.
40	 */
41	const TOKEN_LENGTH = 32;
42
43	/**
44	 * Name of table used for storing tokens
45	 */
46	const TABLE = 'egw_sharing';
47
48	/**
49	 * Reference to global db object
50	 *
51	 * @var Api\Db
52	 */
53	protected static $db;
54
55	/**
56	 * Share we are instanciated for
57	 *
58	 * @var array
59	 */
60	protected $share;
61
62	const READONLY = 'share_ro';
63	const WRITABLE = 'share_rw';
64
65 	/**
66	 * Modes for sharing files
67	 *
68	 * @var array
69	 */
70	static $modes = array(
71		self::READONLY => array(
72			'label' => 'Readonly share',
73			'title' => 'Link is generated allowing recipients to view entries',
74		),
75		self::WRITABLE => array(
76			'label' => 'Writable share',
77			'title' => 'Link is generated allowing recipients to view and modify entries'
78		),
79	);
80
81	/**
82	 * Protected constructor called via self::create_session
83	 *
84	 * @param string $token
85	 * @param array $share
86	 */
87	protected function __construct(array $share)
88	{
89		static::$db = $GLOBALS['egw']->db;
90		$this->share = $share;
91	}
92
93	/**
94	 * Get token from url
95	 */
96	public static function get_token()
97	{
98    // WebDAV has no concept of a query string and clients (including cadaver)
99    // seem to pass '?' unencoded, so we need to extract the path info out
100    // of the request URI ourselves
101    // if request URI contains a full url, remove schema and domain
102		$matches = null;
103    if (preg_match('|^https?://[^/]+(/.*)$|', $path_info=$_SERVER['REQUEST_URI'], $matches))
104    {
105      $path_info = $matches[1];
106    }
107    $path_info = substr($path_info, strlen($_SERVER['SCRIPT_NAME']));
108		list(, $token/*, $path*/) = preg_split('|[/?]|', $path_info, 3);
109
110		list($token) = explode(':', $token);
111		return $token;
112	}
113
114	/**
115	 * Get root of share
116	 *
117	 * @return string
118	 */
119	public function get_root()
120	{
121		return $this->share['share_root'];
122	}
123
124	/**
125	 * Get share path
126	 */
127	public function get_path()
128	{
129		return $this->share['share_path'];
130	}
131
132	/**
133	 * Get share with email addresses
134	 */
135	public function get_share_with()
136	{
137		return $this->share['share_with'];
138	}
139
140	/**
141	 * Create sharing session
142	 *
143	 * Certain cases:
144	 * a) there is not session $keep_session === null
145	 *    --> create new anon session with just filemanager rights and share as fstab
146	 * b) there is a session $keep_session === true
147	 *  b1) current user is share owner (eg. checking the link)
148	 *      --> mount share under token additionally
149	 *  b2) current user not share owner
150	 *  b2a) need/use filemanager UI (eg. directory)
151	 *       --> destroy current session and continue with a)
152	 *  b2b) single file or WebDAV
153	 *       --> modify EGroupware enviroment for that request only, no change in session
154	 *
155	 * @param boolean $keep_session =null null: create a new session, true: try mounting it into existing (already verified) session
156	 * @return string with sessionid
157	 */
158	public static function create_session($keep_session=null)
159	{
160		$share = array();
161		static::check_token($keep_session, $share);
162		if($share)
163		{
164			$classname = static::get_share_class($share);
165			$classname::setup_share($keep_session, $share);
166			return $classname::login($keep_session, $share);
167		}
168		return '';
169	}
170
171	protected static function check_token($keep_session, &$share)
172	{
173		self::$db = $GLOBALS['egw']->db;
174
175		$token = static::get_token();
176
177		// are we called from header include, because session did not verify
178		// --> check if it verifys for our token
179		if ($token && !$keep_session)
180		{
181			$_SERVER['PHP_AUTH_USER'] = $token;
182			if (!isset($_SERVER['PHP_AUTH_PW'])) $_SERVER['PHP_AUTH_PW'] = '';
183
184			unset($GLOBALS['egw_info']['flags']['autocreate_session_callback']);
185			if (isset($GLOBALS['egw']->session) && $GLOBALS['egw']->session->verify()
186				&& isset($GLOBALS['egw']->sharing) && $GLOBALS['egw']->sharing->share['share_token'] === $token)
187			{
188				return $GLOBALS['egw']->session->sessionid;
189			}
190		}
191
192		if (empty($token) || !($share = self::$db->select(self::TABLE, '*', array(
193			'share_token' => $token,
194			'(share_expires IS NULL OR share_expires > '.self::$db->quote(time(), 'date').')',
195		), __LINE__, __FILE__,false,'',Db::API_APPNAME)->fetch()) ||
196			!$GLOBALS['egw']->accounts->exists($share['share_owner']))
197		{
198			sleep(1);
199
200			return static::share_fail(
201				'404 Not Found',
202				"Requested resource '/".htmlspecialchars($token)."' does NOT exist!\n"
203			);
204		}
205		// check password, if required
206		if(!static::check_password($share))
207		{
208			$realm = 'EGroupware share '.$share['share_token'];
209			header('WWW-Authenticate: Basic realm="'.$realm.'"');
210			return static::share_fail(
211				'401 Unauthorized',
212				"Authorization failed."
213			);
214		}
215
216	}
217
218	/**
219	 * Check to see if the share needs a password, and if it does that the password
220	 * provided matches.
221	 *
222	 * @param Array $share
223	 * @return boolean Password OK (or not needed)
224	 */
225	protected static function check_password(Array $share)
226	{
227		if ($share['share_passwd'] && (empty($_SERVER['PHP_AUTH_PW']) ||
228			!(Auth::compare_password($_SERVER['PHP_AUTH_PW'], $share['share_passwd'], 'crypt') ||
229				Header\Authenticate::decode_password($_SERVER['PHP_AUTH_PW']) &&
230					Auth::compare_password($_SERVER['PHP_AUTH_PW'], $share['share_passwd'], 'crypt'))))
231		{
232			return false;
233		}
234		return true;
235	}
236
237	/**
238	 * Sub-class specific things needed to be done to the share before we try
239	 * to login
240	 *
241	 * @param boolean $keep_session
242	 * @param Array $share
243	 */
244	protected static function setup_share($keep_session, &$share) {}
245	/**
246	 * Sub-class specific things needed to be done to the share (or session)
247	 * after we login but before we start actually doing anything
248	 */
249	protected function after_login() {}
250
251
252	protected static function login($keep_session, &$share)
253	{
254		// update accessed timestamp
255		self::$db->update(self::TABLE, array(
256			'share_last_accessed' => $share['share_last_accessed']=time(),
257		), array(
258			'share_id' => $share['share_id'],
259		), __LINE__, __FILE__);
260
261		// store sharing object in egw object and therefore in session
262		$GLOBALS['egw']->sharing = static::factory($share);
263
264		// we have a session we want to keep, but share owner is different from current user and we need filemanager UI, or no session
265		// --> create a new anon session
266		if ($keep_session === false && $GLOBALS['egw']->sharing->need_session() || is_null($keep_session))
267		{
268			$sessionid = static::create_new_session();
269
270			$GLOBALS['egw']->sharing->after_login();
271		}
272		// we have a session we want to keep, but share owner is different from current user and we dont need filemanager UI
273		// --> we dont need session and close it, to not modifiy it
274		elseif ($keep_session === false)
275		{
276			$GLOBALS['egw']->session->commit_session();
277		}
278		// need to store new fstab and vfs_user in session to allow GET requests / downloads via WebDAV
279		$GLOBALS['egw_info']['user']['vfs_user'] = Vfs::$user;
280		$GLOBALS['egw_info']['server']['vfs_fstab'] = Vfs::mount();
281
282		// update modified egw and egw_info again in session, if neccessary
283		if ($keep_session || $sessionid)
284		{
285			$_SESSION[Session::EGW_INFO_CACHE] = $GLOBALS['egw_info'];
286			unset($_SESSION[Session::EGW_INFO_CACHE]['flags']);	// dont save the flags, they change on each request
287
288			$_SESSION[Session::EGW_OBJECT_CACHE] = serialize($GLOBALS['egw']);
289		}
290
291		return $sessionid;
292	}
293
294	public static function create_new_session()
295	{
296		// create session without checking auth: create(..., false, false)
297		if (!($sessionid = $GLOBALS['egw']->session->create('anonymous@'.$GLOBALS['egw_info']['user']['domain'],
298			'', 'text', false, false)))
299		{
300			sleep(1);
301			return static::share_fail(
302				'500 Internal Server Error',
303				"Failed to create session: ".$GLOBALS['egw']->session->reason."\n"
304			);
305		}
306		return $sessionid;
307	}
308
309	/**
310	 * Factory method to instanciate a share
311	 *
312	 * @param array $share
313	 *
314	 * @return Sharing
315	 */
316	public static function factory($share)
317	{
318		$class = static::get_share_class($share);
319
320		return new $class($share);
321	}
322
323	/**
324	 * Get the namespaced class for the given share
325	 *
326	 * @param string $share
327	 */
328	protected static function get_share_class($share)
329	{
330		try
331		{
332			if(self::is_entry($share))
333			{
334				list($app, $id) = explode('::', $share['share_path']);
335				if($app && class_exists('\EGroupware\\'. ucfirst($app) . '\Sharing'))
336				{
337					return '\EGroupware\\'. ucfirst($app) . '\Sharing';
338				}
339				else if(class_exists('\EGroupware\Stylite\Link\Sharing'))
340				{
341					return '\\EGroupware\\Stylite\\Link\\Sharing';
342				}
343			}
344			else if (class_exists ('\EGroupware\Collabora\Wopi') && (int)$share['share_writable'] === \EGroupware\Collabora\Wopi::WOPI_SHARED)
345			{
346				return '\\EGroupware\\Collabora\\Wopi';
347			}
348			else if ((int)$share['share_writable'] == HiddenUploadSharing::HIDDEN_UPLOAD)
349			{
350				return '\\'.__NAMESPACE__ . '\\'. 'Vfs\\HiddenUploadSharing';
351			}
352		}
353		catch(Exception $e){throw $e;}
354		return '\\'.__NAMESPACE__ . '\\'. (self::is_entry($share) ? 'Link' : 'Vfs'). '\\Sharing';
355	}
356
357	/**
358	 * Something failed, stop everything
359	 *
360	 * @param String $status
361	 * @param String $message
362	 */
363	public static function share_fail($status, $message)
364	{
365		header("HTTP/1.1 $status");
366		header("X-WebDAV-Status: $status", true);
367		echo $message;
368
369		$class = strpos($status, '404') === 0 ? 'EGroupware\Api\Exception\NotFound' :
370				strpos($status, '401') === 0 ? 'EGroupware\Api\Exception\NoPermission' :
371				'EGroupware\Api\Exception';
372		throw new $class($message);
373	}
374
375	/**
376	 * Check if we use filemanager UI
377	 *
378	 * Only for directories, if browser supports it and filemanager is installed
379	 *
380	 * @return boolean
381	 */
382	public function use_filemanager()
383	{
384		return !(!Vfs::is_dir($this->share['share_root']) || $_SERVER['REQUEST_METHOD'] != 'GET' ||
385			// or unsupported browsers like ie < 10
386			Header\UserAgent::type() == 'msie' && Header\UserAgent::version() < 10.0 ||
387			// or if no filemanager installed (WebDAV has own autoindex)
388			!file_exists(__DIR__.'/../../filemanager/inc/class.filemanager_ui.inc.php'));
389	}
390	/**
391	 * Check if we should use Collabora UI
392	 *
393	 * Only for files, if URL says so, and Collabora & Stylite apps are installed
394	 */
395	public function use_collabora()
396	{
397		 return !Vfs::is_dir($this->share['share_root']) &&
398				array_key_exists('edit', $_REQUEST) &&
399				array_key_exists('collabora', $GLOBALS['egw_info']['apps']) &&
400				array_key_exists('stylite', $GLOBALS['egw_info']['apps']);
401
402	}
403
404	public function is_entry($share = false)
405	{
406		list($app, $id) = explode('::', $share['share_path']);
407		return $share && $share['share_path'] &&
408				$app && $id && !in_array($app, array('filemanager', 'vfs')) ;//&& array_key_exists($app, $GLOBALS['egw_info']['apps']);
409	}
410
411	public function need_session()
412	{
413		return $this->use_filemanager() || static::is_entry($this->session);
414	}
415
416	/**
417	 * Get actions for sharing an entry from the given app
418	 *
419	 * @param string $appname
420	 * @param int $group Current menu group
421	 */
422	public static function get_actions($appname, $group = 6)
423	{
424		Translation::add_app('api');
425		$actions = array(
426		'share' => array(
427				'caption' => lang('Share'),
428				'icon' => 'api/share',
429				'group' => $group,
430				'allowOnMultiple' => false,
431				'children' => array(
432					'shareReadonlyLink' => array(
433						'caption' => lang('Share link'),
434						'group' => 1,
435						'icon' => 'link',
436						'order' => 11,
437						'enabled' => "javaScript:app.$appname.is_share_enabled",
438						'onExecute' => "javaScript:app.$appname.share_link",
439						'hint' => lang("Share this %1 via URL", Link::get_registry($appname, 'entry'))
440					),
441					'shareWritable' => array(
442						'caption' => lang('Writable'),
443						'group' => 2,
444						'icon' => 'edit',
445						'allowOnMultiple' => true,
446						'enabled' => "javaScript:app.$appname.is_share_enabled",
447						'checkbox' => true,
448						'hint' => lang("Allow editing the %1", Link::get_registry($appname, 'entry'))
449					),
450					'shareFiles' => array(
451						'caption' => lang('Share files'),
452						'group' => 2,
453						'allowOnMultiple' => true,
454						'enabled' => "javaScript:app.$appname.is_share_enabled",
455						'checkbox' => true,
456						'hint' => lang('Include access to any linked files (Links tab)')
457					),
458					'shareFilemanager' => array(
459						'caption' => lang('share filemanager directory'),
460						'group' => 10,
461						'icon' => 'link',
462						'order' => 20,
463						'enabled' => "javaScript:app.$appname.is_share_enabled",
464						'onExecute' => "javaScript:app.$appname.share_link",
465						'hint' => lang('Share just the associated filemanager directory, not the %1', Link::get_registry($appname, 'entry'))
466					),
467				),
468		));
469		if(!$GLOBALS['egw_info']['user']['apps']['filemanager'])
470		{
471			unset($actions['share']['children']['shareFilemanager']);
472		}
473		if(!$GLOBALS['egw_info']['user']['apps']['stylite'])
474		{
475			array_unshift($actions['share']['children'], array(
476				'caption' => lang('EPL Only'),
477				'group' => 0
478			));
479			foreach($actions['share']['children'] as &$child)
480			{
481				$child['enabled'] = false;
482			}
483		}
484		return $actions;
485	}
486
487	/**
488	 * Serve a request on a share specified in REQUEST_URI
489	 */
490	public function ServeRequest()
491	{
492		// sharing is for a different share, change to current share
493		if ($this->share['share_token'] !== self::get_token())
494		{
495			// to keep the session we require the regular user flag "N" AND a user-name not equal to "anonymous"
496			self::create_session($GLOBALS['egw']->session->session_flags === 'N' &&
497				$GLOBALS['egw_info']['user']['account_lid'] !== 'anonymous');
498
499			return $GLOBALS['egw']->sharing->ServeRequest();
500		}
501
502		// No extended ACL for readonly shares, disable eacl by setting session cache
503		if(!($this->share['share_writable'] & 1))
504		{
505			Cache::setSession(Vfs\Sqlfs\StreamWrapper::EACL_APPNAME, 'extended_acl', array(
506				'/' => 1,
507				$this->share['share_path'] => 1
508			));
509		}
510		if($this->use_collabora())
511		{
512			$ui = new \EGroupware\Collabora\Ui();
513			return $ui->editor($this->share['share_path']);
514		}
515		// use pure WebDAV for everything but GET requests to directories
516		else if (!$this->use_filemanager() && !static::is_entry($this->share))
517		{
518			// send a content-disposition header, so browser knows how to name downloaded file
519			if (!Vfs::is_dir($this->share['share_root']))
520			{
521				Header\Content::disposition(Vfs::basename($this->share['share_path']), false);
522			}
523			$GLOBALS['egw']->session->commit_session();
524
525			// WebDAV always looks at the original request for a single file so make sure the file is found at the root
526			Vfs::$is_root = true;
527			unset($GLOBALS['egw_info']['server']['vfs_fstab']);
528			Vfs::mount($this->share['resolve_url'], '/', false, false, true);
529			Vfs::clearstatcache();
530
531			$webdav_server = new Vfs\WebDAV();
532			$webdav_server->ServeRequest(Vfs::concat('/', $this->share['share_token']));
533			return;
534		}
535		return $this->get_ui();
536	}
537
538	/**
539	 * Get the user interface for this share
540	 *
541	 */
542	public function get_ui()
543	{
544		echo 'Error: missing subclass';
545	}
546
547	/**
548	 * Generate a new token
549	 *
550	 * @return string
551	 */
552	public static function token()
553	{
554		// generate random token (using oppenssl if available otherwise mt_rand based Api\Auth::randomstring)
555		do {
556			$token = function_exists('openssl_random_pseudo_bytes') ?
557				base64_encode(openssl_random_pseudo_bytes(3*self::TOKEN_LENGTH/4)) :
558				Auth::randomstring(self::TOKEN_LENGTH);
559			// base64 can contain chars not allowed in our vfs-urls eg. / or #
560		} while ($token != urlencode($token));
561
562		return $token;
563	}
564
565	/**
566	 * Name of the async job for cleaning up shares
567	 */
568	const ASYNC_JOB_ID = 'egw_sharing-tmp_cleanup';
569
570	/**
571	 * Create a new share
572	 *
573	 * @param string $action_id Specific type of share being created, default ''
574	 * @param string $path either path in temp_dir or vfs with optional vfs scheme
575	 * @param string $mode self::LINK: copy file in users tmp-dir or self::READABLE share given vfs file,
576	 *  if no vfs behave as self::LINK
577	 * @param string $name filename to use for $mode==self::LINK, default basename of $path
578	 * @param string|array $recipients one or more recipient email addresses
579	 * @param array $extra =array() extra data to store
580	 * @return array with share data, eg. value for key 'share_token'
581	 * @throw Api\Exception\NotFound if $path not found
582	 * @throw Api\Exception\AssertionFailed if user temp. directory does not exist and can not be created
583	 */
584	public static function create(string $action_id, $path, $mode, $name, $recipients, $extra = array())
585	{
586		if (!isset(static::$db)) static::$db = $GLOBALS['egw']->db;
587
588		if (empty($name)) $name = $path;
589
590		$table_def = static::$db->get_table_definitions(Db::API_APPNAME,static::TABLE);
591		$extra = array_intersect_key($extra, $table_def['fd']);
592
593		// Check if path is mounted somewhere that needs a password
594		static::path_needs_password($path);
595
596		// check if file has been shared before, with identical attributes
597		if (($share = static::$db->select(static::TABLE, '*', $extra+array(
598				'share_path' => $path,
599				'share_owner' => Vfs::$user,
600				'share_expires' => null,
601				'share_passwd'  => null,
602				'share_writable'=> false,
603			), __LINE__, __FILE__, Db::API_APPNAME)->fetch()))
604		{
605			// if yes, just add additional recipients
606			$share['share_with'] = $share['share_with'] ? explode(',', $share['share_with']) : array();
607			$need_save = false;
608			foreach((array)$recipients as $recipient)
609			{
610				if (!in_array($recipient, $share['share_with']))
611				{
612					$share['share_with'][] = $recipient;
613					$need_save = true;
614				}
615			}
616			$share['share_with'] = implode(',', $share['share_with']);
617			if ($need_save)
618			{
619				static::$db->update(static::TABLE, array(
620					'share_with' => $share['share_with'],
621				), array(
622					'share_id' => $share['share_id'],
623				), __LINE__, __FILE__, Db::API_APPNAME);
624			}
625		}
626		else
627		{
628			$i = 0;
629			while(true)	// self::token() can return an existing value
630			{
631				try {
632					static::$db->insert(static::TABLE, $share = array(
633						'share_token' => self::token(),
634						'share_path' => $path,
635						'share_owner' => Vfs::$user,
636						'share_with' => implode(',', (array)$recipients),
637						'share_created' => time(),
638					)+$extra, false, __LINE__, __FILE__, Db::API_APPNAME);
639
640					$share['share_id'] = static::$db->get_last_insert_id(static::TABLE, 'share_id');
641					break;
642				}
643				catch(Db\Exception $e) {
644					if ($i++ > 3) throw $e;
645					unset($e);
646				}
647			}
648		}
649
650		// if not already installed, install periodic cleanup of shares
651		$async = new Asyncservice();
652		$method = 'EGroupware\\Api\\Sharing::tmp_cleanup';
653		if (!($job = $async->read(self::ASYNC_JOB_ID)) || $job[self::ASYNC_JOB_ID]['method'] !== $method)
654		{
655			if ($job) $async->delete(self::ASYNC_JOB_ID);	// update not working old class-name
656
657			$async->set_timer(array('day' => 28), self::ASYNC_JOB_ID, $method ,null);
658		}
659
660		return $share;
661	}
662
663	/**
664	 * Create a share via AJAX
665	 *
666	 * @param String $action
667	 * @param String $path
668	 * @param boolean $writable Allow editing the shared entry / folder / file
669	 * @param boolean $files For sharing an application entry, allow access to the linked files
670	 * @param $extra Additional extra parameters
671	 */
672	public static function ajax_create($action, $path, $writable = false, $files = false, $extra = array())
673	{
674		if(!$path)
675		{
676			throw new Exception\WrongParameter('Missing share path.  Unable to create share.');
677		}
678		$extra = (array)$extra + array(
679			'share_writable' => $writable,
680			'include_files'  => $files
681		);
682		$class = self::get_share_class(array('share_path' => $path) + $extra);
683		$share = $class::create(
684			$action,
685			$path,
686			$writable ? Sharing::WRITABLE : Sharing::READONLY,
687			basename($path),
688			array(),
689			$extra
690		);
691
692		// Store share in session so Merge can find this one and not create a read-only one
693		\EGroupware\Api\Cache::setSession(__CLASS__, $path, $share);
694		$arr = array(
695			'action'		=> $action,
696			'writable'      => $writable,
697			'share_link'	=> $class::share2link($share),
698			'template'		=> Etemplate\Widget\Template::rel2url('/filemanager/templates/default/share_dialog.xet')
699		);
700		switch($action)
701		{
702			case 'shareFilemanager':
703				$arr['title'] = lang('Filemanager directory');
704				break;
705			case 'shareUploadDir':
706			case 'mail_shareUploadDir':
707				$arr['title'] = lang('Upload directory');
708				break;
709		}
710		$response = Json\Response::get();
711		$response->data($arr);
712	}
713
714	/**
715	 * Api\Storage\Base instance for egw_sharing table
716	 *
717	 * @var Api\Storage\Base
718	 */
719	protected static $so;
720
721	/**
722	 * Get a so_sql instance initialised for shares
723	 */
724	public static function so()
725	{
726		if (!isset(self::$so))
727		{
728			self::$so = new Storage\Base('phpgwapi', self::TABLE, null, '', true);
729			self::$so->set_times('string');
730		}
731		return self::$so;
732	}
733
734	/**
735	 * Delete specified shares and unlink temp. files
736	 *
737	 * @param int|array $keys
738	 * @return int number of deleted shares
739	 */
740	public static function delete($keys)
741	{
742		self::$db = $GLOBALS['egw']->db;
743
744		if (is_scalar($keys)) $keys = array('share_id' => $keys);
745
746		// delete specified shares
747		self::$db->delete(self::TABLE, $keys, __LINE__, __FILE__, Db::API_APPNAME);
748		$deleted = self::$db->affected_rows();
749
750		return $deleted;
751	}
752
753	/**
754	 * Home long to keep temp. files: 100 day
755	 */
756	const TMP_KEEP = 8640000;
757	/**
758	 * How long to keep automatic created Wopi shares
759	 */
760	const WOPI_KEEP = '-3month';
761
762	/**.
763	 * Periodic (monthly) cleanup of temporary sharing files (download link)
764	 *
765	 * Exlicit expireds shares are delete, as ones created over 100 days ago and last accessed over 100 days ago.
766	 */
767	public static function tmp_cleanup()
768	{
769		if (!isset(self::$db)) self::$db = $GLOBALS['egw']->db;
770		Vfs::$is_root = true;
771
772		try {
773			$cols = array(
774				'share_path',
775				'MAX(share_expires) AS share_expires',
776				'MAX(share_created) AS share_created',
777				'MAX(share_last_accessed) AS share_last_accessed',
778			);
779			if (($group_concat = self::$db->group_concat('share_id'))) $cols[] = $group_concat.' AS share_id';
780			// remove expired tmp-files unconditionally
781			$having = 'HAVING MAX(share_expires) < '.self::$db->quote(self::$db->to_timestamp(time())).' OR '.
782				// remove without expiration date, when created over 100 days ago AND
783				'MAX(share_expires) IS NULL AND MAX(share_created) < '.self::$db->quote(self::$db->to_timestamp(time()-self::TMP_KEEP)). ' AND '.
784					// (last accessed over 100 days ago OR never)
785					'(MAX(share_last_accessed) IS NULL OR MAX(share_last_accessed) < '.self::$db->quote(self::$db->to_timestamp(time()-self::TMP_KEEP)).')';
786
787			foreach(self::$db->select(self::TABLE, $cols, array(
788				"share_path LIKE '/home/%/.tmp/%'",
789			), __LINE__, __FILE__, false, 'GROUP BY share_path '.$having) as $row)
790			{
791				Vfs::remove($row['share_path']);
792
793				if ($group_concat)
794				{
795					$share_ids = $row['share_id'] ? explode(',', $row['share_id']) : array();
796				}
797				else
798				{
799					$share_ids = array();
800					foreach(self::$db->select(self::TABLE, 'share_id', array(
801						'share_path' => $row['share_path'],
802					), __LINE__, __FILE__) as $id)
803					{
804						$share_ids[] = $id['share_id'];
805					}
806				}
807				if ($share_ids)
808				{
809					$class = self::get_share_class($row);
810					$class::delete(['share_id' => $share_ids]);
811				}
812			}
813
814			// delete automatic created and expired Collabora shares older then 3 month
815			if (class_exists('EGroupware\\Collabora\\Wopi'))
816			{
817				self::$db->delete(self::TABLE, array(
818					'share_expires < '.self::$db->quote(DateTime::to(self::WOPI_KEEP, 'Y-m-d')),
819					'share_writable IN ('.\EGroupware\Collabora\Wopi::WOPI_WRITABLE.','.\EGroupware\Collabora\Wopi::WOPI_READONLY.')',
820				), __LINE__, __FILE__);
821			}
822
823			// Now check the remaining shares
824			static::cleanup_missing_paths();
825		}
826		catch (\Exception $e) {
827			_egw_log_exception($e);
828		}
829		Vfs::$is_root = false;
830	}
831
832	/**
833	 * Check share paths and if the path is no longer there / valid, remove the share
834	 */
835	public static function cleanup_missing_paths()
836	{
837		if (!isset(self::$db)) self::$db = $GLOBALS['egw']->db;
838
839		foreach(self::$db->select(self::TABLE, array(
840			'share_id','share_path', 'share_writable'
841			), array(), __LINE__, __FILE__, false) as $share)
842		{
843			$class = self::get_share_class($share);
844
845			if(!$class::check_path($share))
846			{
847				$class::delete($share);
848			}
849		}
850	}
851
852	/**
853	 * Check that the share path is still valid, and if not, delete it.
854	 * This should be overridden.
855	 *
856	 * @param Array share
857	 *
858	 * @return boolean Is the share still valid
859	 */
860	protected static function check_path($share)
861	{
862		return true;
863	}
864
865	/**
866	 * Generate link from share or share-token
867	 *
868	 * @param string|array $share share or share-token
869	 * @return string full Url incl. schema and host
870	 */
871	public static function share2link($share)
872	{
873		if (is_array($share)) $share = $share['share_token'];
874
875		return Framework::getUrl(Framework::link('/share.php')).'/'.$share;
876	}
877
878	/**
879	 * Check to see if the path has a password required for it's mounting (eg: Samba)
880	 * we need to deal with it specially.  In general, we just throw an exception
881	 * if the mount has $pass in it.
882	 *
883	 * @param string $path
884	 *
885	 * @throws WrongParameter if you try to share a path that needs a password
886	 */
887	public static function path_needs_password($path)
888	{
889		$mounts = array_reverse(Vfs::mount());
890		$parts = Vfs::parse_url($path);
891
892		foreach($mounts as $mounted => $url)
893		{
894			if(($mounted == $parts['path'] || $mounted.'/' == substr($parts['path'],0,strlen($mounted)+1)) && strpos($url, '$pass') !== FALSE)
895			{
896				throw new Exception\WrongParameter(
897					'Cannot share a file that needs a password. (' .
898					$path . ' mounted from '. $url . ')'
899				);
900			}
901		}
902
903		return false;
904	}
905}