1<?php
2/**
3 * EGroupware - Filemanager - user interface
4 *
5 * @link http://www.egroupware.org
6 * @package filemanager
7 * @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
8 * @copyright (c) 2008-17 by Ralf Becker <RalfBecker-AT-outdoor-training.de>
9 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
10 * @version $Id$
11 */
12
13use EGroupware\Api;
14use EGroupware\Api\Egw;
15use EGroupware\Api\Etemplate;
16use EGroupware\Api\Framework;
17use EGroupware\Api\Link;
18use EGroupware\Api\Vfs;
19
20/**
21 * Filemanage user interface class
22 */
23class filemanager_ui
24{
25	/**
26	 * Methods callable via menuaction
27	 *
28	 * @var array
29	 */
30	var $public_functions = array(
31		'index' => true,
32		'file' => true,
33		'editor' => true
34	);
35
36	/**
37	 * Views available from plugins
38	 *
39	 * @var array
40	 */
41	public static $views = array(
42		'filemanager_ui::listview' => 'Listview',
43	);
44	public static $views_init = false;
45
46	/**
47	 * vfs namespace for document merge properties
48	 *
49	 */
50	public static $merge_prop_namespace = '';
51	protected $etemplate;
52	const LIST_TEMPLATE = 'filemanager.index';
53
54	/**
55	 * Constructor
56	 *
57	 */
58	function __construct()
59	{
60		// strip slashes from _GET parameters, if someone still has magic_quotes_gpc on
61		if (get_magic_quotes_gpc() && $_GET)
62		{
63			$_GET = array_stripslashes($_GET);
64		}
65		// do we have root rights
66		if (Api\Cache::getSession('filemanager', 'is_root'))
67		{
68			Vfs::$is_root = true;
69		}
70
71		static::init_views();
72		static::$merge_prop_namespace = Vfs::DEFAULT_PROP_NAMESPACE.$GLOBALS['egw_info']['flags']['currentapp'];
73	}
74
75	/**
76	 * Initialise and return available views
77	 *
78	 * @return array with method => label pairs
79	 */
80	public static function init_views()
81	{
82		if (!static::$views_init)
83		{
84			// translate our labels
85			foreach(static::$views as &$label)
86			{
87				$label = lang($label);
88			}
89			// search for plugins with additional filemanager views
90			foreach(Api\Hooks::process('filemanager_views') as $views)
91			{
92				if (is_array($views)) static::$views += $views;
93			}
94			static::$views_init = true;
95		}
96		return static::$views;
97	}
98
99	/**
100	 * Get active view
101	 *
102	 * @return string
103	 */
104	public static function get_view()
105	{
106		$view =& Api\Cache::getSession('filemanager', 'view');
107		if (isset($_GET['view']))
108		{
109			$view = $_GET['view'];
110		}
111		if (!isset(static::$views[$view]))
112		{
113			reset(static::$views);
114			$view = key(static::$views);
115		}
116		return $view;
117	}
118
119	/**
120	 * Method to build select options out of actions
121	 * @param type $actions
122	 * @return type
123	 */
124	public static function convertActionsToselOptions ($actions)
125	{
126		$sel_options = array ();
127		foreach ($actions as $action => $value)
128		{
129			$sel_options[$action] = array (
130				'label' => $value['caption'],
131				'icon'	=> $value['icon']
132			);
133		}
134		return $sel_options;
135	}
136
137	/**
138	 * Context menu
139	 *
140	 * @return array
141	 */
142	public static function get_actions()
143	{
144		$actions = array(
145			'open' => array(
146				'caption' => lang('Open'),
147				'icon' => '',
148				'group' => $group=1,
149				'allowOnMultiple' => false,
150				'onExecute' => 'javaScript:app.filemanager.open',
151				'default' => true
152			),
153			'new' => array(
154				'caption' => 'New',
155				'group' => $group,
156				'disableClass' => 'noEdit',
157				'children' => array (
158					'document' => array (
159						'caption' => 'Document',
160						'icon' => 'new',
161						'onExecute' => 'javaScript:app.filemanager.create_new',
162					)
163				)
164			),
165			'mkdir' => array(
166				'caption' => lang('Create directory'),
167				'icon' => 'filemanager/button_createdir',
168				'group' => $group,
169				'allowOnMultiple' => false,
170				'disableClass' => 'noEdit',
171				'onExecute' => 'javaScript:app.filemanager.createdir'
172			),
173			'edit' => array(
174				'caption' => lang('Edit settings'),
175				'group' => $group,
176				'allowOnMultiple' => false,
177				'onExecute' => Api\Header\UserAgent::mobile()?'javaScript:app.filemanager.viewEntry':'javaScript:app.filemanager.editprefs',
178				'mobileViewTemplate' => 'file?'.filemtime(Api\Etemplate\Widget\Template::rel2path('/filemanager/templates/mobile/file.xet'))
179			),
180			'saveas' => array(
181				'caption' => lang('Save as'),
182				'group' => $group,
183				'allowOnMultiple' => true,
184				'icon' => 'filesave',
185				'onExecute' => 'javaScript:app.filemanager.force_download',
186				'disableClass' => 'isDir',
187				'enabled' => 'javaScript:app.filemanager.is_multiple_allowed',
188				'shortcut' => array('ctrl' => true, 'shift' => true, 'keyCode' => 83, 'caption' => 'Ctrl + Shift + S'),
189			),
190			'saveaszip' => array(
191				'caption' => lang('Save as ZIP'),
192				'group' => $group,
193				'allowOnMultiple' => true,
194				'icon' => 'save_zip',
195				'postSubmit' => true,
196				'shortcut' => array('ctrl' => true, 'shift' => true, 'keyCode' => 90, 'caption' => 'Ctrl + Shift + Z'),
197			),
198			'egw_paste' => array(
199				'enabled' => false,
200				'group' => $group + 0.5,
201				'hideOnDisabled' => true
202			),
203			'paste' => array(
204				'caption' => lang('Paste'),
205				'acceptedTypes' => 'file',
206				'group' => $group + 0.5,
207				'order' => 10,
208				'enabled' => 'javaScript:app.filemanager.paste_enabled',
209				'children' => array()
210			),
211			'copylink' => array(
212				'caption' => lang('Copy link address'),
213				'group' => $group + 0.5,
214				'icon' => 'copy',
215				'allowOnMultiple' => false,
216				'order' => 10,
217				'onExecute' => 'javaScript:app.filemanager.copy_link'
218			),
219			'share' => EGroupware\Api\Vfs\HiddenUploadSharing::get_actions('filemanager', ++$group)['share'],
220			'documents' => filemanager_merge::document_action(
221				$GLOBALS['egw_info']['user']['preferences']['filemanager']['document_dir'],
222				++$group, 'Insert in document', 'document_',
223				$GLOBALS['egw_info']['user']['preferences']['filemanager']['default_document']
224			),
225			'delete' => array(
226				'caption' => lang('Delete'),
227				'group' => ++$group,
228				'confirm' => 'Delete these files or directories?',
229				'onExecute' => 'javaScript:app.filemanager.action',
230				'disableClass' => 'noDelete'
231			),
232			// DRAG and DROP events
233			'file_drag' => array(
234				'dragType' => array('file','link'),
235				'type' => 'drag',
236				'onExecute' => 'javaScript:app.filemanager.drag'
237			),
238			'file_drop_mail' => array(
239				'type' => 'drop',
240				'acceptedTypes' => 'mail',
241				'onExecute' => 'javaScript:app.filemanager.drop',
242				'hideOnDisabled' => true
243			),
244			'file_drop_move' => array(
245				'icon' => 'stylite/move',
246				'acceptedTypes' => 'file',
247				'caption' => lang('Move into folder'),
248				'type' => 'drop',
249				'onExecute' => 'javaScript:app.filemanager.drop',
250				'default' => true
251			),
252			'file_drop_copy' => array(
253				'icon' => 'stylite/copy',
254				'acceptedTypes' => 'file',
255				'caption' => lang('Copy into folder'),
256				'type' => 'drop',
257				'onExecute' => 'javaScript:app.filemanager.drop'
258			),
259			'file_drop_symlink' => array(
260				'icon' => 'linkpaste',
261				'acceptedTypes' => 'file',
262				'caption' => lang('Link into folder'),
263				'type' => 'drop',
264				'onExecute' => 'javaScript:app.filemanager.drop'
265			)
266		);
267
268		// This one makes no sense in filemanager
269		unset($actions['share']['children']['shareFilemanager']);
270		if (isset($GLOBALS['egw_info']['user']['apps']['mail'])) {
271			$actions['share']['children']['share_mail'] = array(
272				'caption' => lang('Mail'),
273				'icon' => 'mail',
274				'group' => 1,
275				'order' => 0,
276				'allowOnMultiple' => true,
277			);
278			foreach(Vfs\Sharing::$modes as $mode => $data)
279			{
280				$actions['share']['children']['share_mail']['children']['mail_'.$mode] = array(
281					'caption' => $data['label'],
282					'hint' => $data['title'],
283					'icon' => $mode == Vfs\Sharing::ATTACH ?
284						'mail/attach' : 'api/link',
285					'group' => 2,
286					'onExecute' => 'javaScript:app.filemanager.mail',
287				);
288				if ($mode == Vfs\Sharing::ATTACH || $mode == Vfs\Sharing::LINK)
289				{
290					$actions['share']['children']['share_mail']['children']['mail_'.$mode]['disableClass'] = 'isDir';
291				}
292			}
293			foreach(Vfs\HiddenUploadSharing::$modes as $mode => $data)
294			{
295				$actions['share']['children']['share_mail']['children']['mail_shareUploadDir'] = array(
296					'caption' => $data['label'],
297					'hint' => $data['title'],
298					'icon' => 'api/link',
299					'group' => 3,
300					'data' => ['share_writable' => $mode],
301					'enabled' => 'javaScript:app.filemanager.hidden_upload_enabled',
302					'onExecute' => 'javaScript:app.filemanager.mail_share_link',
303				);
304			}
305		}
306
307		// This would be done automatically, but we're overriding
308		foreach($actions as $action_id => $action)
309		{
310			if($action['type'] == 'drop' && $action['caption'])
311			{
312				$action['type'] = 'popup';
313				if($action['acceptedTypes'] == 'file')
314				{
315					$action['enabled'] = 'javaScript:app.filemanager.paste_enabled';
316				}
317				$actions['paste']['children']["{$action_id}_paste"] = $action;
318			}
319		}
320		return $actions;
321	}
322
323	/**
324	 * Get mergeapp property for given path
325	 *
326	 * @param string $path
327	 * @param string $scope (default) or 'parents'
328	 *    $scope == 'self' query only the given path
329	 *    $scope == 'parents' query only path parents for property (first parent in hierarchy upwards wins)
330	 *
331	 * @return string merge application or NULL if no property found
332	 */
333	private static function get_mergeapp($path, $scope='self')
334	{
335		$app = null;
336		switch($scope)
337		{
338			case 'self':
339				$props = Vfs::propfind($path, static::$merge_prop_namespace);
340				$app = empty($props) ? null : $props[0]['val'];
341				break;
342			case 'parents':
343				// search for props in parent directories
344				$currentpath = $path;
345				while($dir = Vfs::dirname($currentpath))
346				{
347					$props = Vfs::propfind($dir, static::$merge_prop_namespace);
348					if(!empty($props))
349					{
350						// found prop in parent directory
351						return $app = $props[0]['val'];
352					}
353					$currentpath = $dir;
354				}
355				break;
356		}
357
358		return $app;
359	}
360
361	/**
362	 * Main filemanager page
363	 *
364	 * @param array $content
365	 * @param string $msg
366	 */
367	function index(array $content=null,$msg=null)
368	{
369		if (!is_array($content))
370		{
371			$content = array(
372				'nm' => Api\Cache::getSession('filemanager', 'index'),
373			);
374			if (!is_array($content['nm']))
375			{
376				$content['nm'] = array(
377					'get_rows'       =>	'filemanager.filemanager_ui.get_rows',	// I  method/callback to request the data for the rows eg. 'notes.bo.get_rows'
378					'filter'         => '',	// current dir only
379					'no_filter2'     => True,	// I  disable the 2. filter (params are the same as for filter)
380					'no_cat'         => True,	// I  disable the cat-selectbox
381					'lettersearch'   => True,	// I  show a lettersearch
382					'searchletter'   =>	false,	// I0 active letter of the lettersearch or false for [all]
383					'start'          =>	0,		// IO position in list
384					'order'          =>	'name',	// IO name of the column to sort after (optional for the sortheaders)
385					'sort'           =>	'ASC',	// IO direction of the sort: 'ASC' or 'DESC'
386					'default_cols'   => '!comment,ctime',	// I  columns to use if there's no user or default pref (! as first char uses all but the named columns), default all columns
387					'csv_fields'     =>	false, // I  false=disable csv export, true or unset=enable it with auto-detected fieldnames,
388									//or array with name=>label or name=>array('label'=>label,'type'=>type) pairs (type is a eT widget-type)
389					'row_id'         => 'path',
390					'row_modified'   => 'mtime',
391					'parent_id'      => 'dir',
392					'is_parent'      => 'is_dir',
393					'favorites'      => true
394				);
395				$content['nm']['path'] = static::get_home_dir();
396			}
397			$content['nm']['actions'] = static::get_actions();
398			$content['nm']['home_dir'] = static::get_home_dir();
399			$content['nm']['view'] = $GLOBALS['egw_info']['user']['preferences']['filemanager']['nm_view'];
400			$content['nm']['placeholder_actions'] = array('mkdir','paste','share','file_drop_mail','file_drop_move','file_drop_copy','file_drop_symlink');
401
402			if (isset($_GET['msg'])) $msg = $_GET['msg'];
403
404			// Blank favorite set via GET needs special handling for path
405			if (isset($_GET['favorite']) && $_GET['favorite'] == 'blank')
406			{
407				$content['nm']['path'] = static::get_home_dir();
408			}
409			// switch to projectmanager folders
410			if (isset($_GET['pm_id']))
411			{
412				$_GET['path'] = '/apps/projectmanager'.((int)$_GET['pm_id'] ? '/'.(int)$_GET['pm_id'] : '');
413			}
414			if (isset($_GET['path']) && ($path = $_GET['path']))
415			{
416				switch($path)
417				{
418					case '..':
419						$path = Vfs::dirname($content['nm']['path']);
420						break;
421					case '~':
422						$path = static::get_home_dir();
423						break;
424				}
425				if ($path && $path[0] == '/' && Vfs::stat($path,true) && Vfs::is_dir($path) && Vfs::check_access($path,Vfs::READABLE))
426				{
427					$content['nm']['path'] = $path;
428				}
429				else
430				{
431					$msg .= lang('The requested path %1 is not available.', $path ? Vfs::decodePath($path) : "false");
432				}
433				// reset lettersearch as it confuses users (they think the dir is empty)
434				$content['nm']['searchletter'] = false;
435				// switch recusive display off
436				if (!$content['nm']['filter']) $content['nm']['filter'] = '';
437			}
438		}
439		$view = static::get_view();
440
441		call_user_func($view,$content,$msg);
442	}
443
444	/**
445	 * Make the current user (vfs) root
446	 *
447	 * The user/pw is either the setup config user or a specially configured vfs_root user
448	 *
449	 * @param string $user setup config user to become root or '' to log off as root
450	 * @param string $password setup config password to become root
451	 * @param boolean &$is_setup=null on return true if authenticated user is setup config user, false otherwise
452	 * @return boolean true is root user given, false otherwise (including logout / empty $user)
453	 */
454	protected function sudo($user='',$password=null,&$is_setup=null)
455	{
456		if (!$user)
457		{
458			$is_root = $is_setup = false;
459		}
460		else
461		{
462			// config user & password
463			$is_setup = Api\Session::user_pw_hash($user,$password) === $GLOBALS['egw_info']['server']['config_hash'];
464			// or vfs root user from setup >> configuration
465			$is_root = $is_setup ||	$GLOBALS['egw_info']['server']['vfs_root_user'] &&
466				in_array($user,preg_split('/, */',$GLOBALS['egw_info']['server']['vfs_root_user'])) &&
467				$GLOBALS['egw']->auth->authenticate($user, $password, 'text');
468		}
469		//error_log(__METHOD__."('$user','$password',$is_setup) user_pw_hash(...)='".Api\Session::user_pw_hash($user,$password)."', config_hash='{$GLOBALS['egw_info']['server']['config_hash']}' --> returning ".array2string($is_root));
470		Api\Cache::setSession('filemanager', 'is_setup',$is_setup);
471		Api\Cache::setSession('filemanager', 'is_root',Vfs::$is_root = $is_root);
472		return Vfs::$is_root;
473	}
474
475	/**
476	 * Filemanager listview
477	 *
478	 * @param array $content
479	 * @param string $msg
480	 */
481	function listview(array $content=null,$msg=null)
482	{
483		$tpl = $this->etemplate ? $this->etemplate : new Etemplate(static::LIST_TEMPLATE);
484
485		if ($msg)
486		{
487			Framework::message($msg);
488		}
489
490		if (($content['nm']['action'] || $content['nm']['rows']) && (empty($content['button']) || !isset($content['button'])))
491		{
492			if ($content['nm']['action'])
493			{
494				$msg = static::action($content['nm']['action'], $content['nm']['selected'], $content['nm']['path']);
495				if ($msg)
496				{
497					Framework::message($msg);
498				}
499
500				// clean up after action
501				unset($content['nm']['selected']);
502				// reset any occasion where action may be stored, as it may be ressurected out of the helpers by etemplate, which is quite unconvenient in case of action delete
503				if (isset($content['nm']['action']))
504				{
505					unset($content['nm']['action']);
506				}
507				if (isset($content['nm']['nm_action']))
508				{
509					unset($content['nm']['nm_action']);
510				}
511				if (isset($content['nm_action']))
512				{
513					unset($content['nm_action']);
514				}
515				// we dont use ['nm']['rows']['delete'], so unset it, if it is present
516				if (isset($content['nm']['rows']['delete']))
517				{
518					unset($content['nm']['rows']['delete']);
519				}
520			}
521			elseif ($content['nm']['rows']['delete'])
522			{
523				$msg = static::action('delete', array_keys($content['nm']['rows']['delete']), $content['nm']['path']);
524				if ($msg)
525				{
526					Framework::message($msg);
527				}
528
529				// clean up after action
530				unset($content['nm']['rows']['delete']);
531				// reset any occasion where action may be stored, as we use ['nm']['rows']['delete'] anyhow
532				// we clean this up, as it may be ressurected out of the helpers by etemplate, which is quite unconvenient in case of action delete
533				if (isset($content['nm']['action']))
534				{
535					unset($content['nm']['action']);
536				}
537				if (isset($content['nm']['nm_action']))
538				{
539					unset($content['nm']['nm_action']);
540				}
541				if (isset($content['nm_action']))
542				{
543					unset($content['nm_action']);
544				}
545				if (isset($content['nm']['selected']))
546				{
547					unset($content['nm']['selected']);
548				}
549			}
550			unset($content['nm']['rows']);
551			Api\Cache::setSession('filemanager', 'index', $content['nm']);
552		}
553
554		// be tolerant with (in previous versions) not correct urlencoded pathes
555		if ($content['nm']['path'][0] == '/' && !Vfs::stat($content['nm']['path'], true) && Vfs::stat(urldecode($content['nm']['path'])))
556		{
557			$content['nm']['path'] = urldecode($content['nm']['path']);
558		}
559		if ($content['button'])
560		{
561			if ($content['button'])
562			{
563				$button = key($content['button']);
564				unset($content['button']);
565			}
566			switch ($button)
567			{
568				case 'upload':
569					if (!$content['upload'])
570					{
571						Framework::message(lang('You need to select some files first!'), 'error');
572						break;
573					}
574					$upload_success = $upload_failure = array();
575					foreach (isset($content['upload'][0]) ? $content['upload'] : array($content['upload']) as $upload)
576					{
577						// encode chars which special meaning in url/vfs (some like / get removed!)
578						$to = Vfs::concat($content['nm']['path'], Vfs::encodePathComponent($upload['name']));
579						if ($upload &&
580								(Vfs::is_writable($content['nm']['path']) || Vfs::is_writable($to)) &&
581								copy($upload['tmp_name'], Vfs::PREFIX . $to))
582						{
583							$upload_success[] = $upload['name'];
584						}
585						else
586						{
587							$upload_failure[] = $upload['name'];
588						}
589					}
590					$content['nm']['msg'] = '';
591					if ($upload_success)
592					{
593						Framework::message(count($upload_success) == 1 && !$upload_failure ? lang('File successful uploaded.') :
594								lang('%1 successful uploaded.', implode(', ', $upload_success)));
595					}
596					if ($upload_failure)
597					{
598						Framework::message(lang('Error uploading file!') . "\n" . etemplate::max_upload_size_message(), 'error');
599					}
600					break;
601			}
602		}
603		$readonlys['button[mailpaste]'] = !isset($GLOBALS['egw_info']['user']['apps']['mail']);
604
605		$sel_options['filter'] = array(
606				'' => 'Current directory',
607				'2' => 'Directories sorted in',
608				'3' => 'Show hidden files',
609				'4' => 'All subdirectories',
610				'5' => 'Files from links',
611				'0' => 'Files from subdirectories',
612		);
613
614		$sel_options['new'] = self::convertActionsToselOptions($content['nm']['actions']['new']['children']);
615
616		// sharing has no divAppbox, we need to set popupMainDiv instead, to be able to drop files everywhere
617		if (substr($_SERVER['SCRIPT_FILENAME'], -10) == '/share.php')
618		{
619			$tpl->setElementAttribute('nm[upload]', 'drop_target', 'popupMainDiv');
620		}
621		// Set view button to match current settings
622		if ($content['nm']['view'] == 'tile')
623		{
624			$tpl->setElementAttribute('nm[button][change_view]', 'statustext', lang('List view'));
625			$tpl->setElementAttribute('nm[button][change_view]', 'image', 'list_row');
626		}
627		// if initial load is done via GET request (idots template or share.php)
628		// get_rows cant call app.filemanager.set_readonly, so we need to do that here
629		if (!array_key_exists('initial_path_readonly', $content))
630		{
631			$content['initial_path_readonly'] = !Vfs::is_writable($content['nm']['path']);
632		}
633
634		$tpl->exec('filemanager.filemanager_ui.index',$content,$sel_options,$readonlys,array('nm' => $content['nm']));
635	}
636
637	/**
638	 * Get the configured start directory for the current user
639	 *
640	 * @return string
641	 */
642	static function get_home_dir()
643	{
644		$start = '/home/'.$GLOBALS['egw_info']['user']['account_lid'];
645
646		// check if user specified a valid startpath in his prefs --> use it
647		if (($path = $GLOBALS['egw_info']['user']['preferences']['filemanager']['startfolder']) &&
648			$path[0] == '/' && Vfs::is_dir($path) && Vfs::check_access($path, Vfs::READABLE))
649		{
650			$start = $path;
651		}
652		elseif (!Vfs::is_dir($start) && Vfs::check_access($start, Vfs::READABLE))
653		{
654			$start = '/';
655		}
656		return $start;
657	}
658
659	/**
660	 * Run a certain action with the selected file
661	 *
662	 * @param string $action
663	 * @param array $selected selected pathes
664	 * @param mixed $dir current directory
665	 * @param int &$errs=null on return number of errors
666	 * @param int &$dirs=null on return number of dirs deleted
667	 * @param int &$files=null on return number of files deleted
668	 * @return string success or failure message displayed to the user
669	 */
670	static public function action($action,$selected,$dir=null,&$errs=null,&$files=null,&$dirs=null)
671	{
672		if (!count($selected))
673		{
674			return lang('You need to select some files first!');
675		}
676		$errs = $dirs = $files = 0;
677
678		switch($action)
679		{
680
681			case 'delete':
682				return static::do_delete($selected,$errs,$files,$dirs);
683
684			case 'mail':
685			case 'copy':
686				foreach($selected as $path)
687				{
688					if (strpos($path, 'mail::') === 0 && $path = substr($path, 6))
689					{
690						// Support for dropping mail in filemanager - Pass mail back to mail app
691						if(ExecMethod2('mail.mail_ui.vfsSaveMessages', $path, $dir))
692						{
693							++$files;
694						}
695						else
696						{
697							++$errs;
698						}
699					}
700					elseif (!Vfs::is_dir($path))
701					{
702						$to = Vfs::concat($dir,Vfs::basename($path));
703						if ($path != $to && Vfs::copy($path,$to))
704						{
705							++$files;
706						}
707						else
708						{
709							++$errs;
710						}
711					}
712					else
713					{
714						$len = strlen(dirname($path));
715						foreach(Vfs::find($path) as $p)
716						{
717							$to = $dir.substr($p,$len);
718							if ($to == $p)	// cant copy into itself!
719							{
720								++$errs;
721								continue;
722							}
723							if (($is_dir = Vfs::is_dir($p)) && Vfs::mkdir($to,null,STREAM_MKDIR_RECURSIVE))
724							{
725								++$dirs;
726							}
727							elseif(!$is_dir && Vfs::copy($p,$to))
728							{
729								++$files;
730							}
731							else
732							{
733								++$errs;
734							}
735						}
736					}
737				}
738				if ($errs)
739				{
740					return lang('%1 errors copying (%2 diretories and %3 files copied)!',$errs,$dirs,$files);
741				}
742				return $dirs ? lang('%1 directories and %2 files copied.',$dirs,$files) : lang('%1 files copied.',$files);
743
744			case 'move':
745				foreach($selected as $path)
746				{
747					$to = Vfs::is_dir($dir) || count($selected) > 1 ? Vfs::concat($dir,Vfs::basename($path)) : $dir;
748					if ($path != $to && Vfs::rename($path,$to))
749					{
750						++$files;
751					}
752					else
753					{
754						++$errs;
755					}
756				}
757				if ($errs)
758				{
759					return lang('%1 errors moving (%2 files moved)!',$errs,$files);
760				}
761				return lang('%1 files moved.',$files);
762
763			case 'symlink':	// symlink given files to $dir
764				foreach((array)$selected as $target)
765				{
766					$link = Vfs::concat($dir, Vfs::basename($target));
767					if (!Vfs::stat($dir) || ($ok = Vfs::mkdir($dir,0,true)))
768					{
769						if(!$ok)
770						{
771							$errs++;
772							continue;
773						}
774					}
775					if ($target[0] != '/') $target = Vfs::concat($dir, $target);
776					if (!Vfs::stat($target))
777					{
778						return lang('Link target %1 not found!', Vfs::decodePath($target));
779					}
780					if ($target != $link && Vfs::symlink($target, $link))
781					{
782						++$files;
783					}
784					else
785					{
786						++$errs;
787					}
788				}
789				if (count((array)$selected) == 1)
790				{
791					return $files ? lang('Symlink to %1 created.', Vfs::decodePath($target)) :
792						lang('Error creating symlink to target %1!', Vfs::decodePath($target));
793				}
794				$ret = lang('%1 elements linked.', $files);
795				if ($errs)
796				{
797					$ret = lang('%1 errors linking (%2)!',$errs, $ret);
798				}
799				return $ret;//." Vfs::symlink('$target', '$link')";
800
801			case 'createdir':
802				$dst = Vfs::concat($dir, is_array($selected) ? $selected[0] : $selected);
803				if (Vfs::mkdir($dst, null, STREAM_MKDIR_RECURSIVE))
804				{
805					return lang("Directory successfully created.");
806				}
807				return lang("Error while creating directory.");
808
809			case 'saveaszip':
810				Vfs::download_zip($selected);
811				exit;
812
813			default:
814				list($action, $settings) = explode('_', $action, 2);
815				switch($action)
816				{
817					case 'document':
818						if (!$settings) $settings = $GLOBALS['egw_info']['user']['preferences']['filemanager']['default_document'];
819						$document_merge = new filemanager_merge(Vfs::decodePath($dir));
820						$msg = $document_merge->download($settings, $selected, '', $GLOBALS['egw_info']['user']['preferences']['filemanager']['document_dir']);
821						if($msg) return $msg;
822						$errs = count($selected);
823						return false;
824				}
825		}
826		return "Unknown action '$action'!";
827	}
828
829	/**
830	 * Delete selected files and return success or error message
831	 *
832	 * @param array $selected
833	 * @param int &$errs=null on return number of errors
834	 * @param int &$dirs=null on return number of dirs deleted
835	 * @param int &$files=null on return number of files deleted
836	 * @return string
837	 */
838	public static function do_delete(array $selected, &$errs=null, &$dirs=null, &$files=null)
839	{
840		$dirs = $files = $errs = 0;
841		// we first delete all selected links (and files)
842		// feeding the links to dirs to Vfs::find() deletes the content of the dirs, not just the link!
843		foreach($selected as $key => $path)
844		{
845			if (!Vfs::is_dir($path) || Vfs::is_link($path))
846			{
847				if (Vfs::unlink($path))
848				{
849					++$files;
850				}
851				else
852				{
853					++$errs;
854				}
855				unset($selected[$key]);
856			}
857		}
858		if ($selected)	// somethings left to delete
859		{
860			// some precaution to never allow to (recursivly) remove /, /apps or /home
861			foreach((array)$selected as $path)
862			{
863				if (Vfs::isProtectedDir($path))
864				{
865					$errs++;
866					return lang("Cautiously rejecting to remove folder '%1'!",Vfs::decodePath($path));
867				}
868			}
869			// now we use find to loop through all files and dirs: (selected only contains dirs now)
870			// - depth=true to get first the files and then the dir containing it
871			// - hidden=true to also return hidden files (eg. Thumbs.db), as we cant delete non-empty dirs
872			// - show-deleted=false to not (finally) deleted versioned files
873			foreach(Vfs::find($selected,array('depth'=>true,'hidden'=>true,'show-deleted'=>false)) as $path)
874			{
875				if (($is_dir = Vfs::is_dir($path) && !Vfs::is_link($path)) && Vfs::rmdir($path,0))
876				{
877					++$dirs;
878				}
879				elseif (!$is_dir && Vfs::unlink($path))
880				{
881					++$files;
882				}
883				else
884				{
885					++$errs;
886				}
887			}
888		}
889		if ($errs)
890		{
891			return lang('%1 errors deleteting (%2 directories and %3 files deleted)!',$errs,$dirs,$files);
892		}
893		if ($dirs)
894		{
895			return lang('%1 directories and %2 files deleted.',$dirs,$files);
896		}
897		return $files == 1 ? lang('File deleted.') : lang('%1 files deleted.',$files);
898	}
899
900	/**
901	 * Callback to fetch the rows for the nextmatch widget
902	 *
903	 * @param array $query
904	 * @param array &$rows
905	 */
906	function get_rows(&$query, &$rows)
907	{
908		$old_session = Api\Cache::getSession('filemanager','index');
909
910		// do NOT store query, if hierarchical data / children are requested
911		if (!$query['csv_export'])
912		{
913			Api\Cache::setSession('filemanager', 'index',
914				array_diff_key ($query, array_flip(array('rows','actions','action_links','placeholder_actions'))));
915		}
916		if(!$query['path']) $query['path'] = static::get_home_dir();
917
918		// Change template to match selected view
919		if($query['view'])
920		{
921			$query['template'] = ($query['view'] == 'row' ? 'filemanager.index.rows' : 'filemanager.tile');
922
923			// Store as preference but only for index, not home
924			if($query['get_rows'] == 'filemanager.filemanager_ui.get_rows')
925			{
926				$GLOBALS['egw']->preferences->add('filemanager','nm_view',$query['view']);
927				$GLOBALS['egw']->preferences->save_repository();
928			}
929		}
930		// be tolerant with (in previous versions) not correct urlencoded pathes
931		if (!Vfs::stat($query['path'],true) && Vfs::stat(urldecode($query['path'])))
932		{
933			$query['path'] = urldecode($query['path']);
934		}
935		if (!Vfs::stat($query['path'],true) || !Vfs::is_dir($query['path']) || !Vfs::check_access($query['path'],Vfs::READABLE))
936		{
937			// only redirect, if it would be to some other location, gives redirect-loop otherwise
938			foreach([$old_session['path'], static::get_home_dir()] as $new_path)
939			{
940				if ($new_path && Vfs::stat($new_path) && $query['path'] != $new_path)
941				{
942					// we will leave here, since we are not allowed, or the location does not exist. Index must handle that, and give
943					// an appropriate message
944					Egw::redirect_link('/index.php', array('menuaction' => 'filemanager.filemanager_ui.index',
945							'path' => $new_path,
946							'msg' => lang('The requested path %1 is not available.', Vfs::decodePath($query['path'])),
947							'ajax' => 'true'
948					));
949					break;
950				}
951			}
952			$rows = array();
953			return 0;
954		}
955		$GLOBALS['egw']->session->commit_session();
956		$rows = $dir_is_writable = array();
957		$vfs_options = $this->get_vfs_options($query);
958		foreach(Vfs::find(!empty($query['col_filter']['dir']) ? $query['col_filter']['dir'] : $query['path'],$vfs_options,true) as $path => $row)
959		{
960			//echo $path; _debug_array($row);
961
962			$dir = dirname($path);
963			if (!isset($dir_is_writable[$dir]))
964			{
965				$dir_is_writable[$dir] = Vfs::is_writable($dir);
966			}
967			if (Vfs::is_dir($path))
968			{
969				if (!isset($dir_is_writable[$path]))
970				{
971					$dir_is_writable[$path] = Vfs::is_writable($path);
972				}
973
974				$row['class'] .= 'isDir ';
975				$row['is_dir'] = 1;
976				if(!$dir_is_writable[$path])
977				{
978					$row['class'] .= 'noEdit ';
979				}
980				if(!Vfs::is_executable($path))
981				{
982					$row['class'] .= 'noExecute';
983				}
984			}
985			elseif (!$dir_is_writable[Vfs::dirname($path)])
986			{
987				$row['class'] .= 'noEdit ';
988			}
989
990			$row['class'] .= !$dir_is_writable[$dir] ? 'noDelete' : '';
991			$row['download_url'] = Vfs::download_url($path);
992			$row['gid'] = -abs($row['gid']);	// gid are positive, but we use negagive account_id for groups internal
993
994			foreach(['mtime','ctime'] as $date_field)
995			{
996				$row[$date_field] = Api\DateTime::server2user($row[$date_field]);
997			}
998			$rows[++$n] = $row;
999			$path2n[$path] = $n;
1000		}
1001		// query comments and cf's for the displayed rows
1002		$cols_to_show = explode(',',$GLOBALS['egw_info']['user']['preferences']['filemanager']['nextmatch-filemanager.index.rows']);
1003
1004		// Always include comment in tiles
1005		if($query['view'] == 'tile')
1006		{
1007			$cols_to_show[] = 'comment';
1008		}
1009		$all_cfs = in_array('customfields',$cols_to_show) && $cols_to_show[count($cols_to_show)-1][0] != '#';
1010		if ($path2n && (in_array('comment',$cols_to_show) || in_array('customfields',$cols_to_show)) &&
1011			($path2props = Vfs::propfind(array_keys($path2n))))
1012		{
1013			foreach($path2props as $path => $props)
1014			{
1015				unset($row);	// fixes a weird problem with php5.1, does NOT happen with php5.2
1016				$row =& $rows[$path2n[$path]];
1017				if ( !is_array($props) ) continue;
1018				foreach($props as $prop)
1019				{
1020					if (!$all_cfs && $prop['name'][0] == '#' && !in_array($prop['name'],$cols_to_show)) continue;
1021					$row[$prop['name']] = strlen($prop['val']) < 64 ? $prop['val'] : substr($prop['val'],0,64).' ...';
1022				}
1023			}
1024		}
1025		// tell client-side if directory is writeable or not
1026		$response = Api\Json\Response::get();
1027		$response->call('app.filemanager.set_readonly', $query['path'], !Vfs::is_writable($query['path']));
1028
1029		//_debug_array($readonlys);
1030		if ($GLOBALS['egw_info']['flags']['currentapp'] == 'projectmanager')
1031		{
1032			// we need our app.css file
1033			if (!file_exists(EGW_SERVER_ROOT.($css_file='/filemanager/templates/'.$GLOBALS['egw_info']['server']['template_set'].'/app.css')))
1034			{
1035				$css_file = '/filemanager/templates/default/app.css';
1036			}
1037			$GLOBALS['egw_info']['flags']['css'] .= "\n\t\t</style>\n\t\t".'<link href="'.$GLOBALS['egw_info']['server']['webserver_url'].
1038				$css_file.'?'.filemtime(EGW_SERVER_ROOT.$css_file).'" type="text/css" rel="StyleSheet" />'."\n\t\t<style>\n\t\t\t";
1039		}
1040		return Vfs::$find_total;
1041	}
1042
1043
1044	/**
1045	 * Get the VFS options (for get rows)
1046	 */
1047	protected function get_vfs_options($query)
1048	{
1049		if($query['searchletter'] && !empty($query['search']))
1050		{
1051			$namefilter = '/^'.$query['searchletter'].'.*'.str_replace(array('\\?','\\*'),array('.{1}','.*'),preg_quote($query['search'])).'/i';
1052			if ($query['searchletter'] == strtolower($query['search'][0]))
1053			{
1054				$namefilter = '/^('.$query['searchletter'].'.*'.str_replace(array('\\?','\\*'),array('.{1}','.*'),preg_quote($query['search'])).'|'.
1055					str_replace(array('\\?','\\*'),array('.{1}','.*'),preg_quote($query['search'])).')/i';
1056			}
1057		}
1058		elseif ($query['searchletter'])
1059		{
1060			$namefilter = '/^'.$query['searchletter'].'/i';
1061		}
1062		elseif(!empty($query['search']))
1063		{
1064			$namefilter = '/'.str_replace(array('\\?','\\*'),array('.{1}','.*'),preg_quote($query['search'])).'/i';
1065		}
1066
1067		// Re-map so 'No filters' favorite ('') is depth 1
1068		$filter = $query['filter'] === '' ? 1 : $query['filter'];
1069
1070		$maxdepth = $filter && $filter != 4 ? (int)(boolean)$filter : null;
1071		if($filter == 5) $maxdepth = 2;
1072		$n = 0;
1073		$vfs_options = array(
1074			'mindepth' => 1,
1075			'maxdepth' => $maxdepth,
1076			'dirsontop' => $filter <= 1,
1077			'type' => $filter && $filter != 5 ? ($filter == 4 ? 'd' : null) : ($filter == 5 ? 'F':'f'),
1078			'order' => $query['order'], 'sort' => $query['sort'],
1079			'limit' => (int)$query['num_rows'].','.(int)$query['start'],
1080			'need_mime' => true,
1081			'name_preg' => $namefilter,
1082			'hidden' => $filter == 3,
1083			'follow' => $filter == 5,
1084		);
1085		if($query['col_filter']['mime'])
1086		{
1087			$vfs_options['mime'] = $query['col_filter']['mime'];
1088		}
1089		if($namefilter)
1090		{
1091			$vfs_options['name'] = $query['search'];
1092		}
1093
1094		return $vfs_options;
1095	}
1096
1097	/**
1098	 * Preferences of a file/directory
1099	 *
1100	 * @param array $content
1101	 * @param string $msg
1102	 */
1103	function file(array $content=null,$msg='')
1104	{
1105		$tpl = new Etemplate('filemanager.file');
1106
1107		if (!is_array($content))
1108		{
1109			if (isset($_GET['msg']))
1110			{
1111				$msg .= $_GET['msg'];
1112			}
1113			if (!($path = str_replace(array('#','?'),array('%23','%3F'),$_GET['path'])) ||	// ?, # need to stay encoded!
1114				// actions enclose pathes containing comma with "
1115				($path[0] == '"' && substr($path,-1) == '"' && !($path = substr(str_replace('""','"',$path),1,-1))) ||
1116				!($stat = Vfs::lstat($path)))
1117			{
1118				$msg .= lang('File or directory not found!')." path='$path', stat=".array2string($stat);
1119			}
1120			else
1121			{
1122				$content = $stat;
1123				$content['name'] = $content['itempicker_merge']['name'] = Vfs::basename($path);
1124				$content['dir'] = $content['itempicker_merge']['dir'] = ($dir = Vfs::dirname($path)) ? Vfs::decodePath($dir) : '';
1125				$content['path'] = $path;
1126				$content['hsize'] = Vfs::hsize($stat['size']);
1127				$content['mime'] = Vfs::mime_content_type($path);
1128				$content['gid'] *= -1;	// our widgets use negative gid's
1129				if (($props = Vfs::propfind($path)))
1130				{
1131					foreach($props as $prop)
1132					{
1133						$content[$prop['name']] = $prop['val'];
1134					}
1135				}
1136				if (($content['is_link'] = Vfs::is_link($path)))
1137				{
1138					$content['symlink'] = Vfs::readlink($path);
1139				}
1140			}
1141			$content['tabs'] = $_GET['tabs'];
1142			if (!($content['is_dir'] = Vfs::is_dir($path) && !Vfs::is_link($path)))
1143			{
1144				$content['perms']['executable'] = (int)!!($content['mode'] & 0111);
1145				$mask = 6;
1146				if (preg_match('/^text/',$content['mime']) && $content['size'] < 100000)
1147				{
1148					$content['text_content'] = file_get_contents(Vfs::PREFIX.$path);
1149				}
1150			}
1151			else
1152			{
1153				//currently not implemented in backend $content['perms']['sticky'] = (int)!!($content['mode'] & 0x201);
1154				$mask = 7;
1155			}
1156			foreach(array('owner' => 6,'group' => 3,'other' => 0) as $name => $shift)
1157			{
1158				$content['perms'][$name] = ($content['mode'] >> $shift) & $mask;
1159			}
1160			$content['is_owner'] = Vfs::has_owner_rights($path,$content);
1161		}
1162		else
1163		{
1164			//_debug_array($content);
1165			$path =& $content['path'];
1166
1167			$button = @key($content['button']);
1168			unset($content['button']);
1169			if(!$button && $content['sudo'])
1170			{
1171				// Button to stop sudo is not in button namespace
1172				$button = 'sudo';
1173				unset($content['sudo']);
1174			}
1175			// need to check 'setup' button (submit button in sudo popup), as some browsers (eg. chrome) also fill the hidden field
1176			if ($button == 'sudo' && Vfs::$is_root || $button == 'setup' && $content['sudo']['user'])
1177			{
1178				$msg = $this->sudo($button == 'setup' ? $content['sudo']['user'] : '',$content['sudo']['passwd']) ?
1179					lang('Root access granted.') : ($button == 'setup' && $content['sudo']['user'] ?
1180					lang('Wrong username or password!') : lang('Root access stopped.'));
1181				unset($content['sudo']);
1182				$content['is_owner'] = Vfs::has_owner_rights($path);
1183			}
1184			if (in_array($button,array('save','apply')))
1185			{
1186				$props = array();
1187				$perm_changed = $perm_failed = 0;
1188				foreach($content['old'] as $name => $old_value)
1189				{
1190					if (isset($content[$name]) && ($old_value != $content[$name] ||
1191						// do not check for modification, if modify_subs is checked!
1192						$content['modify_subs'] && in_array($name,array('uid','gid','perms'))) &&
1193						($name != 'uid' || Vfs::$is_root))
1194					{
1195						if ($name == 'name')
1196						{
1197							if (!($dir = Vfs::dirname($path)))
1198							{
1199								$msg .= lang('File or directory not found!')." Vfs::dirname('$path')===false";
1200								if ($button == 'save') $button = 'apply';
1201								continue;
1202							}
1203							$to = Vfs::concat($dir, $content['name']);
1204							if (file_exists(Vfs::PREFIX.$to) && $content['confirm_overwrite'] !== $to)
1205							{
1206								$tpl->set_validation_error('name',lang("There's already a file with that name!").'<br />'.
1207									lang('To overwrite the existing file store again.',lang($button)));
1208								$content['confirm_overwrite'] = $to;
1209								if ($button == 'save') $button = 'apply';
1210								continue;
1211							}
1212							if (Vfs::rename($path,$to))
1213							{
1214								$msg .= lang('Renamed %1 to %2.',Vfs::decodePath(basename($path)),Vfs::decodePath(basename($to))).' ';
1215								$content['old']['name'] = $content[$name];
1216								$path = $to;
1217								$content['mime'] = Api\MimeMagic::filename2mime($path);	// recheck mime type
1218								$refresh_path = Vfs::dirname($path);	// for renames, we have to refresh the parent
1219							}
1220							else
1221							{
1222								$msg .= lang('Rename of %1 to %2 failed!',Vfs::decodePath(basename($path)),Vfs::decodePath(basename($to))).' ';
1223								if (Vfs::deny_script($to))
1224								{
1225									$msg .= lang('You are NOT allowed to upload a script!').' ';
1226								}
1227							}
1228						}
1229						elseif ($name[0] == '#' || $name == 'comment')
1230						{
1231							$props[] = array('name' => $name, 'val' => $content[$name] ? $content[$name] : null);
1232						}
1233						elseif ($name == 'mergeapp')
1234						{
1235							$mergeprop = array(
1236								array(
1237									'ns'	=> static::$merge_prop_namespace,
1238									'name'	=> 'mergeapp',
1239									'val'	=> (!empty($content[$name]) ? $content[$name] : null),
1240								),
1241							);
1242							if (Vfs::proppatch($path,$mergeprop))
1243							{
1244								$content['old'][$name] = $content[$name];
1245								$msg .= lang('Setting for document merge saved.');
1246							}
1247							else
1248							{
1249								$msg .= lang('Saving setting for document merge failed!');
1250							}
1251						}
1252						else
1253						{
1254							static $name2cmd = array('uid' => 'chown','gid' => 'chgrp','perms' => 'chmod');
1255							$cmd = array('EGroupware\\Api\\Vfs',$name2cmd[$name]);
1256							$value = $name == 'perms' ? static::perms2mode($content['perms']) : $content[$name];
1257							if(!$value) continue;
1258							if ($content['modify_subs'])
1259							{
1260								if ($name == 'perms')
1261								{
1262									$changed = Vfs::find($path,array('type'=>'d'),$cmd,array($value));
1263									$changed += Vfs::find($path,array('type'=>'f'),$cmd,array($value & 0666));	// no execute for files
1264								}
1265								else
1266								{
1267									$changed = Vfs::find($path,null,$cmd,array($value));
1268								}
1269								$ok = $failed = 0;
1270								foreach($changed as $sub_path => &$r)
1271								{
1272									if ($r)
1273									{
1274										++$ok;
1275										// Changing owner does not change mtime.  Clear subs on UI so they get reloaded
1276										if($sub_path == $path) continue;
1277										Api\Json\Response::get()->apply('egw.dataStoreUID',['filemanager::'.$sub_path,null]);
1278									}
1279									else
1280									{
1281										++$failed;
1282									}
1283								}
1284								if ($ok && !$failed)
1285								{
1286									if(!$perm_changed++) $msg .= lang('Permissions of %1 changed.',$path.' '.lang('and all it\'s childeren'));
1287									$content['old'][$name] = $content[$name];
1288								}
1289								elseif($failed)
1290								{
1291									if(!$perm_failed++) $msg .= lang('Failed to change permissions of %1!',$path.lang('and all it\'s childeren').
1292										($ok ? ' ('.lang('%1 failed, %2 succeded',$failed,$ok).')' : ''));
1293								}
1294							}
1295							elseif (call_user_func_array($cmd,array($path,$value)))
1296							{
1297								$msg .= lang('Permissions of %1 changed.',$path);
1298								$content['old'][$name] = $content[$name];
1299							}
1300							else
1301							{
1302								$msg .= lang('Failed to change permissions of %1!',$path);
1303							}
1304						}
1305					}
1306				}
1307				if ($props)
1308				{
1309					if (Vfs::proppatch($path,$props))
1310					{
1311						foreach($props as $prop)
1312						{
1313							$content['old'][$prop['name']] = $prop['val'];
1314						}
1315						$msg .= lang('Properties saved.');
1316					}
1317					else
1318					{
1319						$msg .= lang('Saving properties failed!');
1320					}
1321				}
1322			}
1323			elseif ($content['eacl'] && $content['is_owner'])
1324			{
1325				if ($content['eacl']['delete'])
1326				{
1327					$ino_owner = key($content['eacl']['delete']);
1328					list(, $owner) = explode('-',$ino_owner,2);	// $owner is a group and starts with a minus!
1329					$msg .= Vfs::eacl($path,null,$owner) ? lang('ACL deleted.') : lang('Error deleting the ACL entry!');
1330				}
1331				elseif ($button == 'eacl')
1332				{
1333					if (!$content['eacl_owner'])
1334					{
1335						$msg .= lang('You need to select an owner!');
1336					}
1337					else
1338					{
1339						$msg .= Vfs::eacl($path,$content['eacl']['rights'],$content['eacl_owner']) ?
1340							lang('ACL added.') : lang('Error adding the ACL!');
1341					}
1342				}
1343			}
1344			Framework::refresh_opener($msg, 'filemanager', $refresh_path ? $refresh_path : $path, 'edit', null, '&path=[^&]*');
1345			if ($button == 'save') Framework::window_close();
1346		}
1347		if ($content['is_link'] && !Vfs::stat($path))
1348		{
1349			$msg .= ($msg ? "\n" : '').lang('Link target %1 not found!',$content['symlink']);
1350		}
1351		$content['link'] = Egw::link(Vfs::download_url($path));
1352		$content['icon'] = Vfs::mime_icon($content['mime']);
1353		$content['msg'] = $msg;
1354
1355		if (($readonlys['uid'] = !Vfs::$is_root) && !$content['uid']) $content['ro_uid_root'] = 'root';
1356		// only owner can change group & perms
1357		if (($readonlys['gid'] = !$content['is_owner'] ||
1358			Vfs::parse_url(Vfs::resolve_url($content['path']),PHP_URL_SCHEME) == 'oldvfs') ||// no uid, gid or perms in oldvfs
1359				 !Vfs::is_writable($path))
1360		{
1361			if (!$content['gid']) $content['ro_gid_root'] = 'root';
1362			foreach($content['perms'] as $name => $value)
1363			{
1364				$readonlys['perms['.$name.']'] = true;
1365			}
1366		}
1367		$readonlys['gid'] = $readonlys['gid'] || !Vfs::is_writable($path);
1368		$readonlys['name'] = $path == '/' || !($dir = Vfs::dirname($path)) || !Vfs::is_writable($dir);
1369		$readonlys['comment'] = !Vfs::is_writable($path);
1370		$readonlys['tabs']['filemanager.file.preview'] = $readonlys['tabs']['filemanager.file.perms'] = $content['is_link'];
1371
1372		// Don't allow permission changes for these, even for root - it causes too many problems.
1373		// Use the CLI if you really need to make changes
1374		if(in_array($content['path'], ['/','/home','/apps']))
1375		{
1376			foreach($content['perms'] as $name => $value)
1377			{
1378				$readonlys['perms['.$name.']'] = true;
1379			}
1380			$readonlys['gid'] = true;
1381			$readonlys['uid'] = true;
1382			$readonlys['modify_subs'] = true;
1383		}
1384
1385		// if neither owner nor is writable --> disable save&apply
1386		$readonlys['button[save]'] = $readonlys['button[apply]'] = !$content['is_owner'] && !Vfs::is_writable($path);
1387
1388		if (!($cfs = Api\Storage\Customfields::get('filemanager')))
1389		{
1390			$readonlys['tabs']['custom'] = true;
1391		}
1392		elseif (!Vfs::is_writable($path))
1393		{
1394			foreach($cfs as $name => $data)
1395			{
1396				$readonlys['#'.$name] = true;
1397			}
1398		}
1399		$readonlys['tabs']['filemanager.file.eacl'] = true;	// eacl off by default
1400		if ($content['is_dir'])
1401		{
1402			$readonlys['tabs']['filemanager.file.preview'] = true;	// no preview tab for dirs
1403			$sel_options['rights']=$sel_options['owner']=$sel_options['group']=$sel_options['other'] = array(
1404				7 => lang('Display and modification of content'),
1405				5 => lang('Display of content'),
1406				0 => lang('No access'),
1407			);
1408			if(($content['eacl'] = Vfs::get_eacl($content['path'])) !== false &&	// backend supports eacl
1409				$GLOBALS['egw_info']['user']['account_id'] == Vfs::$user)	// leave eACL tab disabled for sharing
1410			{
1411				unset($readonlys['tabs']['filemanager.file.eacl']);	// --> switch the tab on again
1412				foreach($content['eacl'] as &$eacl)
1413				{
1414					$eacl['path'] = rtrim(Vfs::parse_url($eacl['path'],PHP_URL_PATH),'/');
1415					$readonlys['delete['.$eacl['ino'].'-'.$eacl['owner'].']'] = $eacl['ino'] != $content['ino'] ||
1416						$eacl['path'] != $content['path'] || !$content['is_owner'];
1417				}
1418				array_unshift($content['eacl'],false);	// make the keys start with 1, not 0
1419				$content['eacl']['owner'] = 0;
1420				$content['eacl']['rights'] = 5;
1421			}
1422		}
1423		else
1424		{
1425			$sel_options['owner']=$sel_options['group']=$sel_options['other'] = array(
1426				6 => lang('Read & write access'),
1427				4 => lang('Read access only'),
1428				0 => lang('No access'),
1429			);
1430		}
1431
1432		// Times are in server time, convert to user timezone
1433		foreach(['mtime','ctime'] as $date_field)
1434		{
1435			$time = new Api\DateTime($content[$date_field],Api\DateTime::$server_timezone);
1436			$time->setUser();
1437			$content[$date_field] = $time->format('ts');
1438		}
1439
1440		// mergeapp
1441		$content['mergeapp'] = static::get_mergeapp($path, 'self');
1442		$content['mergeapp_parent'] = static::get_mergeapp($path, 'parents');
1443		if(!empty($content['mergeapp']))
1444		{
1445			$content['mergeapp_effective'] = $content['mergeapp'];
1446		}
1447		elseif(!empty($content['mergeapp_parent']))
1448		{
1449			$content['mergeapp_effective'] = $content['mergeapp_parent'];
1450		}
1451		else
1452		{
1453			$content['mergeapp_effective'] = null;
1454		}
1455		// mergeapp select options
1456		$mergeapp_list = Link::app_list('merge');
1457		unset($mergeapp_list[$GLOBALS['egw_info']['flags']['currentapp']]); // exclude filemanager from list
1458		$mergeapp_empty = !empty($content['mergeapp_parent'])
1459			? $mergeapp_list[$content['mergeapp_parent']] . ' (parent setting)' : '';
1460		$sel_options['mergeapp'] = array(''	=> $mergeapp_empty);
1461		$sel_options['mergeapp'] = $sel_options['mergeapp'] + $mergeapp_list;
1462		// mergeapp other gui options
1463		$content['mergeapp_itempicker_disabled'] = $content['is_dir'] || empty($content['mergeapp_effective']);
1464
1465		$preserve = $content;
1466		if (!isset($preserve['old']))
1467		{
1468			$preserve['old'] = array(
1469				'perms' => $content['perms'],
1470				'name'  => $content['name'],
1471				'uid'   => $content['uid'],
1472				'gid'   => $content['gid'],
1473				'comment' => (string)$content['comment'],
1474				'mergeapp' => $content['mergeapp']
1475			);
1476			if ($cfs) foreach($cfs as $name => $data)
1477			{
1478				$preserve['old']['#'.$name] = (string)$content['#'.$name];
1479			}
1480		}
1481		if (Vfs::$is_root)
1482		{
1483			$tpl->setElementAttribute('sudouser', 'label', 'Logout');
1484			$tpl->setElementAttribute('sudouser', 'help','Log out as superuser');
1485			// Need a more complex submit because button type is buttononly, which doesn't submit
1486			$tpl->setElementAttribute('sudouser', 'onclick','app.filemanager.set_sudoButton(widget,"login")');
1487
1488		}
1489		elseif ($button == 'sudo')
1490		{
1491			$tpl->setElementAttribute('sudouser', 'label', 'Superuser');
1492			$tpl->setElementAttribute('sudouser', 'help','Enter setup user and password to get root rights');
1493			$tpl->setElementAttribute('sudouser', 'onclick','app.filemanager.set_sudoButton(widget,"logout")');
1494		}
1495		else if (self::is_anonymous($GLOBALS['egw_info']['user']['account_id']))
1496		{
1497			// Just hide sudo for anonymous users
1498			$readonlys['sudouser'] = true;
1499		}
1500		if (($extra_tabs = Vfs::getExtraInfo($path,$content)))
1501		{
1502			// add to existing tabs in template
1503			$tpl->setElementAttribute('tabs', 'add_tabs', true);
1504
1505			$tabs =& $tpl->getElementAttribute('tabs','tabs');
1506			if (true) $tabs = array();
1507
1508			foreach(isset($extra_tabs[0]) ? $extra_tabs : array($extra_tabs) as $extra_tab)
1509			{
1510				$tabs[] = array(
1511					'label' =>	$extra_tab['label'],
1512					'template' =>	$extra_tab['name']
1513				);
1514				if ($extra_tab['data'] && is_array($extra_tab['data']))
1515				{
1516					$content = array_merge($content, $extra_tab['data']);
1517				}
1518				if ($extra_tab['preserve'] && is_array($extra_tab['preserve']))
1519				{
1520					$preserve = array_merge($preserve, $extra_tab['preserve']);
1521				}
1522				if ($extra_tab['readonlys'] && is_array($extra_tab['readonlys']))
1523				{
1524					$readonlys = array_merge($readonlys, $extra_tab['readonlys']);
1525				}
1526			}
1527		}
1528		Framework::window_focus();
1529		$GLOBALS['egw_info']['flags']['app_header'] = lang('Preferences').' '.Vfs::decodePath($path);
1530
1531		$tpl->exec('filemanager.filemanager_ui.file',$content,$sel_options,$readonlys,$preserve,2);
1532	}
1533
1534	/**
1535	 * Check if the user is anonymous user
1536	 * @param integer $account_id
1537	 */
1538	protected static function is_anonymous($account_id)
1539	{
1540		$acl = new Api\Acl($account_id);
1541		$acl->read_repository();
1542		return $acl->check('anonymous', 1, 'phpgwapi');
1543	}
1544
1545	/**
1546	 * Run given action on given path(es) and return array/object with values for keys 'msg', 'errs', 'dirs', 'files'
1547	 *
1548	 * @param string $action eg. 'delete', ...
1549	 * @param array $selected selected path(s)
1550	 * @param string $dir=null current directory
1551	 * @see static::action()
1552	 */
1553	public static function ajax_action($action, $selected, $dir=null, $props=null)
1554	{
1555		// do we have root rights, need to run here too, as method is static and therefore does NOT run __construct
1556		if (Api\Cache::getSession('filemanager', 'is_root'))
1557		{
1558			Vfs::$is_root = true;
1559		}
1560		$response = Api\Json\Response::get();
1561
1562		$arr = array(
1563			'msg' => '',
1564			'action' => $action,
1565			'errs' => 0,
1566			'dirs' => 0,
1567			'files' => 0,
1568		);
1569
1570		if (!isset($dir)) $dir = array_pop($selected);
1571
1572		switch($action)
1573		{
1574			case 'upload':
1575				static::handle_upload_action($action, $selected, $dir, $props, $arr);
1576				break;
1577			case 'shareWritableLink':
1578			case 'shareReadonlyLink':
1579				if ($action === 'shareWritableLink')
1580				{
1581					$share = Vfs\Sharing::create(
1582							'', $selected, Vfs\Sharing::WRITABLE, basename($selected), array(), array('share_writable' => true)
1583					);
1584				}
1585				else
1586				{
1587					$share = Vfs\Sharing::create(
1588							'', $selected, Vfs\Sharing::READONLY, basename($selected), array()
1589					);
1590				}
1591				$arr["share_link"] = $link = Vfs\Sharing::share2link($share);
1592				$arr["template"] = Api\Etemplate\Widget\Template::rel2url('/filemanager/templates/default/share_dialog.xet');
1593				break;
1594
1595			// Upload, then link
1596			case 'link':
1597				// First upload
1598				$arr = static::ajax_action('upload', $selected, $dir, $props);
1599				$app_dir = Link::vfs_path($props['entry']['app'],$props['entry']['id'],'',true);
1600
1601				foreach($arr['uploaded'] as $file)
1602				{
1603					$target=Vfs::concat($dir,Vfs::encodePathComponent($file['name']));
1604					if (Vfs::file_exists($target) && $app_dir)
1605					{
1606						if (!Vfs::file_exists($app_dir)) Vfs::mkdir($app_dir);
1607						error_log("Symlinking $target to $app_dir");
1608						Vfs::symlink($target, Vfs::concat($app_dir,Vfs::encodePathComponent($file['name'])));
1609					}
1610				}
1611				// Must return to avoid adding to $response again
1612				return;
1613
1614			default:
1615				$arr['msg'] = static::action($action, $selected, $dir, $arr['errs'], $arr['dirs'], $arr['files']);
1616		}
1617		$response->data($arr);
1618		//error_log(__METHOD__."('$action',".array2string($selected).') returning '.array2string($arr));
1619		return $arr;
1620	}
1621
1622	/**
1623	 * Deal with an uploaded file
1624	 *
1625	 * @param string $action Should be 'upload'
1626	 * @param $selected Array of file information
1627	 * @param string $dir Target directory
1628	 * @param $props
1629	 * @param string[] $arr Result
1630	 *
1631	 * @throws Api\Exception\AssertionFailed
1632	 */
1633	protected static function handle_upload_action(string $action, $selected, $dir, $props, &$arr)
1634	{
1635		$script_error = 0;
1636		$conflict = $selected['conflict'];
1637		unset($selected['conflict']);
1638
1639		foreach($selected as $tmp_name => &$data)
1640		{
1641			$path = Vfs::concat($dir, Vfs::encodePathComponent($data['name']));
1642
1643			if(Vfs::deny_script($path))
1644			{
1645				if (!isset($script_error))
1646				{
1647					$arr['msg'] .= ($arr['msg'] ? "\n" : '').lang('You are NOT allowed to upload a script!');
1648				}
1649				++$script_error;
1650				++$arr['errs'];
1651				unset($selected[$tmp_name]);
1652				continue;
1653			}
1654			elseif (Vfs::is_dir($path))
1655			{
1656				$data['confirm'] = 'is_dir';
1657				continue;
1658			}
1659			elseif (!$data['confirmed'] && Vfs::stat($path))
1660			{
1661				// File exists, what to do?
1662				switch($conflict)
1663				{
1664					case 'overwrite':
1665						unset($data['confirm']);
1666						$data['confirmed'] = true;
1667						break;
1668					case 'rename':
1669						// Find a unique name
1670						$i = 1;
1671						$info = pathinfo($path);
1672						while(Vfs::file_exists($path))
1673						{
1674							$path = $info['dirname'] . '/'. $info['filename'] . " ($i)." . $info['extension'];
1675							$i++;
1676						}
1677						break;
1678					case 'ask':
1679					default:
1680						$data['confirm'] = true;
1681				}
1682			}
1683			if(!$data['confirm'])
1684			{
1685				if (is_dir($GLOBALS['egw_info']['server']['temp_dir']) && is_writable($GLOBALS['egw_info']['server']['temp_dir']))
1686				{
1687					$tmp_path = $GLOBALS['egw_info']['server']['temp_dir'] . '/' . basename($tmp_name);
1688				}
1689				else
1690				{
1691					$tmp_path = ini_get('upload_tmp_dir') . '/' . basename($tmp_name);
1692				}
1693
1694				if (Vfs::copy_uploaded($tmp_path, $path, $props, false))
1695				{
1696					++$arr['files'];
1697					$uploaded[] = $data['name'];
1698				}
1699				else
1700				{
1701					++$arr['errs'];
1702				}
1703			}
1704		}
1705		if ($arr['errs'] > $script_error)
1706		{
1707			$arr['msg'] .= ($arr['msg'] ? "\n" : '').lang('Error uploading file!');
1708		}
1709		if ($arr['files'])
1710		{
1711			$arr['msg'] .= ($arr['msg'] ? "\n" : '').lang('%1 successful uploaded.', implode(', ', $uploaded));
1712		}
1713		$arr['uploaded'] = $selected;
1714		$arr['path'] = $dir;
1715		$arr['props'] = $props;
1716	}
1717
1718	/**
1719	 * Convert perms array back to integer mode
1720	 *
1721	 * @param array $perms with keys owner, group, other, executable, sticky
1722	 * @return int
1723	 */
1724	private function perms2mode(array $perms)
1725	{
1726		$mode = $perms['owner'] << 6 | $perms['group'] << 3 | $perms['other'];
1727		if ($mode['executable'])
1728		{
1729			$mode |= 0111;
1730		}
1731		if ($mode['sticky'])
1732		{
1733			$mode |= 0x201;
1734		}
1735		return $mode;
1736	}
1737}
1738