1/**
2 * EGroupware - Filemanager - Javascript UI
3 *
4 * @link http://www.egroupware.org
5 * @package filemanager
6 * @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
7 * @copyright (c) 2008-19 by Ralf Becker <RalfBecker-AT-outdoor-training.de>
8 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
9 */
10
11/*egw:uses
12	/api/js/jsapi/egw_app.js;
13 */
14import {EgwApp} from "../../api/js/jsapi/egw_app";
15import {et2_nextmatch} from "../../api/js/etemplate/et2_extension_nextmatch";
16
17/**
18 * UI for filemanager
19 */
20export class filemanagerAPP extends EgwApp
21{
22	/**
23	 * path widget, by template
24	 */
25	path_widget: {} = {};
26	/**
27	 * Are files cut into clipboard - need to be deleted at source on paste
28	 */
29	clipboard_is_cut: boolean = false;
30
31	/**
32	 * Regexp to convert id to a path, use this.id2path(_id)
33	 */
34	private remove_prefix : RegExp = /^filemanager::/;
35
36	private readonly;
37
38	/**
39	 * Constructor
40	 *
41	 * @memberOf app.filemanager
42	 */
43	constructor()
44	{
45		// call parent
46		super('filemanager');
47
48		// Loading filemanager in its tab and home causes us problems with
49		// unwanted destruction, so we check for already existing path widgets
50		let lists = etemplate2.getByApplication('home');
51		for (let i = 0; i < lists.length; i++)
52		{
53			if(lists[i].app == 'filemanager' && lists[i].widgetContainer.getWidgetById('path'))
54			{
55				this.path_widget[lists[i].uniqueId] = lists[i].widgetContainer.getWidgetById('path');
56			}
57		}
58	}
59
60	/**
61	 * Destructor
62	 */
63	destroy(_app)
64	{
65		delete this.et2;
66
67		// call parent
68		super.destroy(_app)
69	}
70
71	/**
72	 * This function is called when the etemplate2 object is loaded
73	 * and ready.  If you must store a reference to the et2 object,
74	 * make sure to clean it up in destroy().
75	 *
76	 * @param et2 etemplate2 Newly ready object
77	 * @param {string} name template name
78	 */
79	et2_ready(et2,name)
80	{
81		// call parent
82		super.et2_ready(et2, name);
83
84		let path_widget = this.et2.getWidgetById('path');
85		if(path_widget)	// do NOT set not found path-widgets, as uploads works on first one only!
86		{
87			this.path_widget[et2.DOMContainer.id] = path_widget;
88			// Bind to removal to remove from list
89			jQuery(et2.DOMContainer).on('clear', function(e) {
90				if (app.filemanager && app.filemanager.path_widget) delete app.filemanager.path_widget[e.target.id];
91			});
92		}
93
94		if(this.et2.getWidgetById('nm'))
95		{
96			// Legacy JS only supports 2 arguments (event and widget), so set
97			// to the actual function here
98			this.et2.getWidgetById('nm').set_onfiledrop(jQuery.proxy(this.filedrop, this));
99		}
100
101		// get clipboard from browser localstore and update button tooltips
102		this.clipboard_tooltips();
103
104		// calling set_readonly for initial path
105		if (this.et2.getArrayMgr('content').getEntry('initial_path_readonly'))
106		{
107			this.readonly = [this.et2.getArrayMgr('content').getEntry('nm[path]'), true];
108		}
109		if (typeof this.readonly != 'undefined')
110		{
111			this.set_readonly.apply(this, this.readonly);
112			delete this.readonly;
113		}
114
115		if (name == 'filemanager.index')
116		{
117			let fe = egw.link_get_registry('filemanager-editor');
118			let new_widget =  this.et2.getWidgetById('new');
119			if (fe && fe["edit"])
120			{
121				let new_options = this.et2.getArrayMgr('sel_options').getEntry('new');
122				new_widget.set_select_options(new_options);
123			}
124			else if (new_widget)
125			{
126				new_widget.set_disabled(true);
127			}
128		}
129	}
130
131	/**
132	 * Set the application's state to the given state.
133	 *
134	 * Extended from parent to also handle view
135	 *
136	 *
137	 * @param {{name: string, state: object}|string} state Object (or JSON string) for a state.
138	 *	Only state is required, and its contents are application specific.
139	 *
140	 * @return {boolean} false - Returns false to stop event propagation
141	 */
142	setState(state)
143	{
144		// State should be an object, not a string, but we'll parse
145		if(typeof state == "string")
146		{
147			if(state.indexOf('{') != -1 || state =='null')
148			{
149				state = JSON.parse(state);
150			}
151		}
152		let result = super.setState(state, 'filemanager.index');
153
154		// This has to happen after the parent, changing to tile recreates
155		// nm controller
156		if(typeof state == "object" && state.state && state.state.view)
157		{
158			let et2 = etemplate2.getById('filemanager-index');
159			if(et2)
160			{
161				this.et2 = et2.widgetContainer;
162				this.change_view(state.state.view);
163			}
164		}
165		return result;
166	}
167
168	/**
169	 * Retrieve the current state of the application for future restoration
170	 *
171	 * Extended from parent to also set view
172	 *
173	 * @return {object} Application specific map representing the current state
174	 */
175	getState()
176	{
177		let state = super.getState();
178
179		let et2 = etemplate2.getById('filemanager-index');
180		if(et2)
181		{
182			let nm = et2.widgetContainer.getWidgetById('nm');
183			state.view = nm.view;
184		}
185		return state;
186	}
187
188	/**
189	 * Convert id to path (remove "filemanager::" prefix)
190	 */
191	id2path(_id : string) : string
192	{
193		return _id.replace(this.remove_prefix, '');
194	}
195
196	/**
197	 * Convert array of elems to array of paths
198	 */
199	_elems2paths(_elems) : string[]
200	{
201		let paths = [];
202		for (let i = 0; i < _elems.length; i++)
203		{
204			// If selected has no id, try parent.  This happens for the placeholder row
205			// in empty directories.
206			paths.push(_elems[i].id? this.id2path(_elems[i].id) : _elems[i]._context._parentId);
207		}
208		return paths;
209	}
210
211	/**
212	 * Get directory of a path
213	 */
214	dirname(_path : string) : string
215	{
216		let parts = _path.split('/');
217		parts.pop();
218		return parts.join('/') || '/';
219	}
220
221	/**
222	 * Get name of a path
223	 */
224	basename(_path : string) : string
225	{
226		return _path.split('/').pop();
227	}
228
229	/**
230	 * Get current working directory
231	 */
232	get_path(etemplate_name? : string) : string
233	{
234		if(!etemplate_name || typeof this.path_widget[etemplate_name] == 'undefined')
235		{
236			for(etemplate_name in this.path_widget) break;
237		}
238		let path_widget = this.path_widget[etemplate_name];
239		return path_widget ? path_widget.get_value.apply(path_widget) : null;
240	}
241
242	/**
243	 * Open compose with already attached files
244	 *
245	 * @param {(string|string[])} attachments path(s)
246	 * @param {object} params
247	 */
248	open_mail(attachments : string | string[], params? : object)
249	{
250		if (typeof attachments == 'undefined') attachments = this.get_clipboard_files();
251		if (!params || typeof params != 'object') params = {};
252		if (!(attachments instanceof Array)) attachments = [ attachments ];
253		let content = {data:{files:{file:[]}}};
254		for(let i=0; i < attachments.length; i++)
255		{
256			params['preset[file]['+i+']'] = 'vfs://default'+attachments[i];
257			content.data.files.file.push('vfs://default'+attachments[i]);
258		}
259		content.data.files["filemode"] = params['preset[filemode]'];
260		// always open compose in html mode, as attachment links look a lot nicer in html
261		params["mimeType"] = 'html';
262		return egw.openWithinWindow("mail", "setCompose", content, params, /mail.mail_compose.compose/);
263	}
264
265	/**
266	 * Mail files action: open compose with already attached files
267	 *
268	 * @param _action
269	 * @param _elems
270	 */
271	mail(_action, _elems)
272	{
273		this.open_mail(this._elems2paths(_elems), {
274			'preset[filemode]': _action.id.substr(5)
275		});
276	}
277
278	/**
279	 * Mail files action: open compose with already linked files
280	 * We're only interested in hidden upload shares here, open_mail can handle
281	 * the rest
282	 *
283	 * @param {egwAction} _action
284	 * @param {egwActionObject[]} _selected
285	 */
286	mail_share_link(_action, _selected)
287	{
288		if(_action.id !== 'mail_shareUploadDir')
289		{
290			return this.mail(_action, _selected);
291		}
292		let path = this.id2path(_selected[0].id);
293
294		this.share_link(_action, _selected, null, false, false, this._mail_link_callback);
295
296		return true;
297	}
298
299	/**
300	 * Callback with the share link to append to an email
301	 *
302	 * @param {Object} _data
303	 * @param {String} _data.share_link Link to the share
304	 * @param {String} _data.title Title for the link
305	 * @param {String} [_data.msg] Error message
306	 */
307	_mail_link_callback(_data)
308	{
309		debugger;
310		if (_data.msg || !_data.share_link) window.egw_refresh(_data.msg, this.appname);
311
312		let params = {
313			'preset[body]': '<a href="'+_data.share_link + '">'+_data.title+'</a>',
314			'mimeType': 'html'// always open compose in html mode, as attachment links look a lot nicer in html
315		};
316		let content = {
317			mail_htmltext: ['<br /><a href="'+_data.share_link + '">'+_data.title+'</a>'],
318			mail_plaintext: ["\n"+_data.share_link]
319		};
320		return egw.openWithinWindow("mail", "setCompose", content, params, /mail.mail_compose.compose/);
321	}
322
323	/**
324	 * Trigger Upload after each file is uploaded
325	 * @param {type} _event
326	 */
327	uploadOnOne(_event)
328	{
329		this.upload(_event,1);
330	}
331
332	/**
333	 * Send names of uploaded files (again) to server, to process them: either copy to vfs or ask overwrite/rename
334	 *
335	 * @param {event} _event
336	 * @param {number} _file_count
337	 * @param {string=} _path where the file is uploaded to, default current directory
338	 * @param {string} _conflict What to do if the file conflicts with one on the server
339	 * @param {string} _target Upload processing target.  Sharing classes can override this.
340	 */
341	upload(_event, _file_count : number, _path? : string, _conflict = "ask", _target: string = 'filemanager_ui::ajax_action')
342	{
343		if(typeof _path == 'undefined')
344		{
345			_path = this.get_path();
346		}
347		if (_file_count && !jQuery.isEmptyObject(_event.data.getValue()))
348		{
349			let widget = _event.data;
350			let value = widget.getValue();
351			value.conflict = _conflict;
352			egw.json(_target, ['upload', value, _path],
353				this._upload_callback, this, true, this
354			).sendRequest();
355			widget.set_value('');
356		}
357	}
358
359	/**
360	 * Finish callback for file a file dialog, to get the overwrite / rename prompt
361	 *
362	 * @param {event} _event
363	 * @param {number} _file_count
364	 */
365	file_a_file_upload(_event, _file_count : number) : boolean
366	{
367		let widget = _event.data;
368		let path = widget.getRoot().getWidgetById("path").getValue();
369		let action = widget.getRoot().getWidgetById("action").getValue();
370		let link = widget.getRoot().getWidgetById("entry").getValue();
371		if(action == 'save_as' && link.app && link.id)
372		{
373			path = "/apps/"+link.app+"/"+link.id;
374		}
375
376		let props = widget.getInstanceManager().getValues(widget.getRoot());
377		egw.json('filemanager_ui::ajax_action', [action == 'save_as' ? 'upload' : 'link', widget.getValue(), path, props],
378			function(_data)
379			{
380				app.filemanager._upload_callback(_data);
381
382				// Remove successful after a delay
383				for(var file in _data.uploaded)
384				{
385					if(!_data.uploaded[file].confirm || _data.uploaded[file].confirmed)
386					{
387						// Remove that file from file widget...
388						widget.remove_file(_data.uploaded[file].name);
389					}
390				}
391				opener.egw_refresh('','filemanager',null,null,'filemanager');
392			}, app.filemanager, true, this
393		).sendRequest(true);
394		return true;
395	}
396
397
398	/**
399	 * Callback for server response to upload request:
400	 * - display message and refresh list
401	 * - ask use to confirm overwritting existing files or rename upload
402	 *
403	 * @param {object} _data values for attributes msg, files, ...
404	 */
405	_upload_callback(_data)
406	{
407		if(_data.msg || _data.uploaded) window.egw_refresh(_data.msg, this.appname, undefined, undefined, undefined, undefined, undefined, _data.type);
408
409		let that = this;
410		for (let file in _data.uploaded)
411		{
412			if(_data.uploaded[file].confirm && !_data.uploaded[file].confirmed)
413			{
414				let buttons = [
415					{
416						text: this.egw.lang("Yes"),
417						id: "overwrite",
418						class: "ui-priority-primary",
419						"default": true,
420						image: 'check'
421					},
422					{text: this.egw.lang("Rename"), id: "rename", image: 'edit'},
423					{text: this.egw.lang("Cancel"), id: "cancel"}
424				];
425				if (_data.uploaded[file].confirm === "is_dir")
426					buttons.shift();
427				let dialog = et2_dialog.show_prompt(function(_button_id, _value) {
428						let uploaded = {};
429						uploaded[this.my_data.file] = this.my_data.data;
430						switch (_button_id)
431						{
432							case "overwrite":
433								uploaded[this.my_data.file].confirmed = true;
434							// fall through
435							case "rename":
436								uploaded[this.my_data.file].name = _value;
437								delete uploaded[this.my_data.file].confirm;
438								// send overwrite-confirmation and/or rename request to server
439								egw.json('filemanager_ui::ajax_action', [this.my_data.action, uploaded, this.my_data.path, this.my_data.props],
440									that._upload_callback, that, true, that
441								).sendRequest();
442								return;
443							case "cancel":
444								// Remove that file from every file widget...
445								that.et2.iterateOver(function(_widget) {
446									_widget.remove_file(this.my_data.data.name);
447								}, this, et2_file);
448						}
449					},
450					_data.uploaded[file].confirm === "is_dir" ?
451						this.egw.lang("There's already a directory with that name!") :
452						this.egw.lang('Do you want to overwrite existing file %1 in directory %2?', _data.uploaded[file].name, _data.path),
453					this.egw.lang('File %1 already exists', _data.uploaded[file].name),
454					_data.uploaded[file].name, buttons, file);
455				// setting required data for callback in as my_data
456				dialog.my_data = {
457					action: _data.action,
458					file: file,
459					path: _data.path,
460					data: _data.uploaded[file],
461					props: _data.props
462				};
463			}
464		}
465	}
466
467	/**
468	 * Get any files that are in the system clipboard
469	 *
470	 * @return {string[]} Paths
471	 */
472	get_clipboard_files()
473	{
474		let clipboard_files = [];
475		if (typeof window.localStorage != 'undefined' && typeof egw.getSessionItem('phpgwapi', 'egw_clipboard') != 'undefined')
476		{
477			let clipboard = JSON.parse(egw.getSessionItem('phpgwapi', 'egw_clipboard')) || {
478				type:[],
479				selected:[]
480			};
481			if(clipboard.type.indexOf('file') >= 0)
482			{
483				for(let i = 0; i < clipboard.selected.length; i++)
484				{
485					let split = clipboard.selected[i].id.split('::');
486					if(split[0] == 'filemanager')
487					{
488						clipboard_files.push(this.id2path(clipboard.selected[i].id));
489					}
490				}
491			}
492		}
493		return clipboard_files;
494	}
495
496	/**
497	 * Update clickboard tooltips in buttons
498	 */
499	clipboard_tooltips()
500	{
501		let paste_buttons = ['button[paste]', 'button[linkpaste]', 'button[mailpaste]'];
502		for(let i=0; i < paste_buttons.length; ++i)
503		{
504			let button = this.et2.getWidgetById(paste_buttons[i]);
505			if (button) button.set_statustext(this.get_clipboard_files().join(",\n"));
506		}
507	}
508
509	/**
510	 * Clip files into clipboard
511	 *
512	 * @param _action
513	 * @param _elems
514	 */
515	clipboard(_action, _elems)
516	{
517		this.clipboard_is_cut = _action.id == "cut";
518		let clipboard = JSON.parse(egw.getSessionItem('phpgwapi', 'egw_clipboard')) || {
519			type:[],
520			selected:[]
521		};
522		if(_action.id != "add")
523		{
524			clipboard = {
525				type:[],
526				selected:[]
527			};
528		}
529
530		// When pasting we need to know the type of data - pull from actions
531		let drag = _elems[0].getSelectedLinks('drag').links;
532		for(let k in drag)
533		{
534			if(drag[k].enabled && drag[k].actionObj.dragType.length > 0)
535			{
536				clipboard.type = clipboard.type.concat(drag[k].actionObj.dragType);
537			}
538		}
539		clipboard.type = jQuery.unique(clipboard.type);
540		// egwAction is a circular structure and can't be stringified so just take what we want
541		// Hopefully that's enough for the action handlers
542		for(let k in _elems)
543		{
544			if(_elems[k].id) clipboard.selected.push({id:_elems[k].id, data:_elems[k].data});
545		}
546
547		// Save it in session
548		egw.setSessionItem('phpgwapi', 'egw_clipboard', JSON.stringify(clipboard));
549
550		this.clipboard_tooltips();
551	}
552
553	/**
554	 * Paste files into current directory or mail them
555	 *
556	 * @param _type 'paste', 'linkpaste', 'mailpaste'
557	 */
558	paste(_type : string)
559	{
560		let clipboard_files = this.get_clipboard_files();
561		if (clipboard_files.length == 0)
562		{
563			alert(this.egw.lang('Clipboard is empty!'));
564			return;
565		}
566		switch(_type)
567		{
568			case 'mailpaste':
569				this.open_mail(clipboard_files);
570				break;
571
572			case 'paste':
573				this._do_action(this.clipboard_is_cut ? 'move' : 'copy', clipboard_files);
574
575				if (this.clipboard_is_cut)
576				{
577					this.clipboard_is_cut = false;
578					clipboard_files = [];
579					this.clipboard_tooltips();
580				}
581				break;
582
583			case 'linkpaste':
584				this._do_action('symlink', clipboard_files);
585				break;
586		}
587	}
588
589	/**
590	 * Pass action to server
591	 *
592	 * @param _action
593	 * @param _elems
594	 */
595	action(_action, _elems)
596	{
597		let paths = this._elems2paths(_elems);
598		let path = this.get_path(_action && _action.parent.data.nextmatch.getInstanceManager().uniqueId || false);
599		this._do_action(_action.id, paths,true, path);
600	}
601
602	/**
603	 * Prompt user for directory to create
604	 *
605	 * @param {egwAction|undefined} action Action, event or undefined if called directly
606	 * @param {egwActionObject[] | undefined} selected Selected row, or undefined if called directly
607	 */
608	createdir(action, selected)
609	{
610		let self = this;
611		et2_dialog.show_prompt(function(button, dir){
612			if (button && dir)
613			{
614				let path = self.get_path(action && action.parent ? action.parent.data.nextmatch.getInstanceManager().uniqueId : false);
615				if(action && action instanceof egwAction)
616				{
617					let paths = self._elems2paths(selected);
618					if(paths[0]) path = paths[0];
619					// check if target is a file --> use it's directory instead
620					if(selected[0].id || path)
621					{
622						let data = egw.dataGetUIDdata(selected[0].id || 'filemanager::'+path );
623						if (data && data.data.mime != 'httpd/unix-directory')
624						{
625							path = self.dirname(path);
626						}
627					}
628				}
629				self._do_action('createdir', egw.encodePathComponent(dir), true, path);	// true=synchronous request
630				self.change_dir((path == '/' ? '' : path)+'/'+ egw.encodePathComponent(dir));
631			}
632		},this.egw.lang('New directory'),this.egw.lang('Create directory'));
633	}
634
635	/**
636	 * Prompt user for directory to create
637	 */
638	symlink()
639	{
640		let self = this;
641		et2_dialog.show_prompt(function (button, target) {
642			if (button && target)
643			{
644				self._do_action('symlink', target);
645			}
646		},this.egw.lang('Link target'), this.egw.lang('Create link'));
647	}
648
649	/**
650	 * Run a serverside action via an ajax call
651	 *
652	 * @param _type 'move_file', 'copy_file', ...
653	 * @param _selected selected paths
654	 * @param _sync send a synchronous ajax request
655	 * @param _path defaults to current path
656	 */
657	_do_action(_type, _selected, _sync?, _path?)
658	{
659		if (typeof _path == 'undefined') _path = this.get_path();
660		egw.json('filemanager_ui::ajax_action', [_type, _selected, _path],
661			this._do_action_callback, this, !_sync, this
662		).sendRequest();
663	}
664
665	/**
666	 * Callback for _do_action ajax call
667	 *
668	 * @param _data
669	 */
670	_do_action_callback(_data)
671	{
672		window.egw_refresh(_data.msg, this.appname, undefined, undefined, undefined, undefined, undefined, _data.type);
673	}
674
675	/**
676	 * Force download of a file by appending '?download' to it's download url
677	 *
678	 * @param _action
679	 * @param _senders
680	 */
681	force_download(_action, _senders) : boolean
682	{
683		for(let i = 0; i < _senders.length; i++)
684		{
685			let data = egw.dataGetUIDdata(_senders[i].id);
686			let url = data ? data.data.download_url : '/webdav.php'+this.id2path(_senders[i].id);
687			if (url[0] == '/') url = egw.link(url);
688
689			let a = document.createElement('a');
690			if(typeof a.download == "undefined")
691			{
692				window.location = <Location><unknown>(url+"?download");
693				return false;
694			}
695
696			// Multiple file download for those that support it
697			let $a = jQuery(a)
698				.prop('href', url)
699				.prop('download', data ? data.data.name : "")
700				.appendTo(this.et2.getDOMNode());
701
702			window.setTimeout(jQuery.proxy(function() {
703				let evt = document.createEvent('MouseEvent');
704				evt.initMouseEvent('click', true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null);
705				this[0].dispatchEvent(evt);
706				this.remove();
707			}, $a), 100*i);
708		}
709		return false;
710	}
711
712	/**
713	 * Check to see if the browser supports downloading multiple files
714	 * (using a tag download attribute) to enable/disable the context menu
715	 *
716	 * @param {egwAction} action
717	 * @param {egwActionObject[]} selected
718	 */
719	is_multiple_allowed(action, selected) : boolean
720	{
721		let allowed = typeof document.createElement('a').download != "undefined";
722
723		if(typeof action == "undefined") return allowed;
724
725		return (allowed || selected.length <= 1) && action.not_disableClass.apply(action, arguments);
726	}
727
728
729	/**
730	 * Change directory
731	 *
732	 * @param {string} _dir directory to change to incl. '..' for one up
733	 * @param {et2_widget} widget
734	 */
735	change_dir(_dir, widget?)
736	{
737		for(var etemplate_name in this.path_widget) break;
738		if (widget) etemplate_name = widget.getInstanceManager().uniqueId;
739
740		// Make sure everything is in place for changing directory
741		if(!this.et2 || typeof etemplate_name !== 'string' ||
742			typeof this.path_widget[etemplate_name] === 'undefined')
743		{
744			return false;
745		}
746
747		switch (_dir)
748		{
749			case '..':
750				_dir = this.dirname(this.get_path(etemplate_name));
751				break;
752			case '~':
753				_dir = this.et2.getWidgetById('nm').options.settings.home_dir;
754				break;
755		}
756
757		this.path_widget[etemplate_name].set_value(_dir);
758	}
759
760	/**
761	 * Toggle view between tiles and rows
762	 *
763	 * @param {string|Event} [view] - Specify what to change the view to.  Either 'tile' or 'row'.
764	 *	Or, if this is used as a callback view is actually the event, and we need to find the view.
765	 * @param {et2_widget} [button_widget] - The widget that's calling
766	 */
767	change_view(view, button_widget?)
768	{
769		let et2 = etemplate2.getById('filemanager-index');
770		let nm : et2_nextmatch;
771		if(et2 && et2.widgetContainer.getWidgetById('nm'))
772		{
773			nm = et2.widgetContainer.getWidgetById('nm');
774		}
775		if(!nm)
776		{
777			egw.debug('warn', 'Could not find nextmatch to change view');
778
779			return;
780		}
781
782		if(!button_widget)
783		{
784			button_widget = (<et2_nextmatch><unknown>nm).getWidgetById('button[change_view]');
785		}
786		if(button_widget && button_widget.instanceOf(et2_button))
787		{
788			// Switch view based on button icon, since controller can get re-created
789			if(typeof view != 'string')
790			{
791				view = button_widget.options.image.replace('list_','');
792			}
793
794			// Toggle button icon to the other view
795			//todo: nm.controller needs to be changed to nm.getController after merging typescript branch into master
796			button_widget.set_image("list_"+(view == et2_nextmatch_controller.VIEW_ROW ? et2_nextmatch_controller.VIEW_TILE : et2_nextmatch_controller.VIEW_ROW));
797
798			button_widget.set_statustext(view == et2_nextmatch_controller.VIEW_ROW ? this.egw.lang("Tile view") : this.egw.lang('List view'));
799		}
800
801		nm.set_view(view);
802		// Put it into active filters (but don't refresh)
803		nm.activeFilters["view"]= view;
804
805		// Change template to match
806		let template : any = view == et2_nextmatch_controller.VIEW_ROW ? 'filemanager.index.rows' : 'filemanager.tile';
807		nm.set_template(template);
808
809		// Wait for template to load, then refresh
810		template = nm.getWidgetById(template);
811		if(template && template.loading)
812		{
813			template.loading.done(function() {
814				nm.applyFilters({view: view});
815			});
816		}
817	}
818
819	/**
820	 * Open/active an item
821	 *
822	 * @param _action
823	 * @param _senders
824	 */
825	open(_action, _senders)
826	{
827		let data = egw.dataGetUIDdata(_senders[0].id);
828		let path = this.id2path(_senders[0].id);
829		this.et2 = this.et2 ? this.et2 : etemplate2.getById('filemanager-index').widgetContainer;
830		let mime = this.et2._inst.widgetContainer.getWidgetById('$row');
831		// try to get mime widget DOM node out of the row DOM
832		let mime_dom = jQuery(_senders[0].iface.getDOMNode()).find("span#filemanager-index_\\$row");
833		let fe = egw_get_file_editor_prefered_mimes();
834
835		// symlinks dont have mime 'http/unix-directory', but server marks all directories with class 'isDir'
836		if (data.data.mime == 'httpd/unix-directory' || data.data['class'] && data.data['class'].split(/ +/).indexOf('isDir') != -1)
837		{
838			this.change_dir(path,_action.parent.data.nextmatch || this.et2);
839		}
840		else if(mime && data.data.mime.match(mime.mime_regexp) && mime_dom.length>0)
841		{
842			mime_dom.click();
843		}
844		else if (mime && this.isEditable(_action, _senders) && fe && fe.edit)
845		{
846
847			egw.open_link(egw.link('/index.php', {
848				menuaction: fe.edit.menuaction,
849				path: decodeURIComponent(data.data.download_url)
850			}), '', fe.edit_popup);
851		}
852		else
853		{
854			let url;
855			// Build ViewerJS url
856			if (data.data.mime.match(/application\/vnd\.oasis\.opendocument/) &&
857				egw.preference('document_doubleclick_action', 'filemanager') == 'collabeditor')
858			{
859				url = '/ViewerJS/#..' + data.data.download_url;
860			}
861
862			egw.open({path: path, type: data.data.mime, download_url: url}, 'file','view',null,'_browser');
863		}
864		return false;
865	}
866
867	/**
868	 * Edit prefs of current directory
869	 *
870	 * @param _action
871	 * @param _senders
872	 */
873	editprefs(_action, _senders)
874	{
875		let path =  typeof _senders != 'undefined' ? this.id2path(_senders[0].id) : this.get_path(_action && _action.parent.data.nextmatch.getInstanceManager().uniqueId || false);
876
877		egw().open_link(egw.link('/index.php', {
878			menuaction: 'filemanager.filemanager_ui.file',
879			path: path
880		}), 'fileprefs', '510x425');
881	}
882
883	/**
884	 * Callback to check if the paste action is enabled.  We also update the
885	 * clipboard historical targets here as well
886	 *
887	 * @param {egwAction} _action  drop action we're checking
888	 * @param {egwActionObject[]} _senders selected files
889	 * @param {egwActionObject} _target Drop or context menu activated on this one
890	 *
891	 * @returns boolean true if enabled, false otherwise
892	 */
893	paste_enabled(_action, _senders, _target)
894	{
895		// Need files in the clipboard for this
896		let clipboard_files = this.get_clipboard_files();
897		if(clipboard_files.length === 0)
898		{
899			return false;
900		}
901
902		// Parent action (paste) gets run through here as well, but needs no
903		// further processing
904		if(_action.id == 'paste') return true;
905
906		if(_action.canHaveChildren.indexOf('drop') == -1)
907		{
908			_action.canHaveChildren.push('drop');
909		}
910		let actions = [];
911
912		// Current directory
913		let current_dir = this.get_path();
914		let dir = egw.dataGetUIDdata('filemanager::'+current_dir);
915		let path_widget = etemplate2.getById('filemanager-index').widgetContainer.getWidgetById('button[createdir]');
916		actions.push({
917			id:_action.id+'_current', caption: current_dir, path: current_dir,
918			enabled: dir && dir.data && dir.data.class && dir.data.class.indexOf('noEdit') === -1 ||
919				!dir && path_widget && !path_widget.options.readonly
920		});
921
922		// Target, if directory
923		let target_dir = this.id2path(_target.id);
924		dir = egw.dataGetUIDdata(_target.id);
925		actions.push({
926			             id: _action.id + '_target',
927			             caption: target_dir,
928			             path: target_dir,
929			             enabled: _target && _target.iface && jQuery(_target.iface.getDOMNode()).hasClass('isDir') &&
930				             (dir && dir.data && dir.data.class && dir.data.class.indexOf('noEdit') === -1 || !dir)
931		             });
932
933		// Last 10 folders
934		let previous_dsts = jQuery.extend([], <any><unknown>egw.preference('drop_history', this.appname));
935		let action_index = 0;
936		for (let i = 0; i < 10; i++)
937		{
938			let path = i < previous_dsts.length ? previous_dsts[i] : '';
939			actions.push({
940				             id: _action.id + '_target_' + action_index++,
941				             caption: path,
942				             path: path,
943				             group: 2,
944				             enabled: path && !(current_dir && path === current_dir || target_dir && path === target_dir)
945			});
946		}
947
948		// Common stuff, every action needs these
949		for(let i = 0; i < actions.length; i++)
950		{
951			//actions[i].type = 'drop',
952			actions[i].acceptedTypes = _action.acceptedTypes;
953			actions[i].no_lang = true;
954			actions[i].hideOnDisabled = true;
955		}
956
957		_action.updateActions(actions);
958
959		// Create paste action
960		// This injects the clipboard data and calls the original handler
961		let paste_exec = function(action, selected) {
962			// Add in clipboard as a sender
963			let clipboard = JSON.parse(egw.getSessionItem('phpgwapi', 'egw_clipboard'));
964
965			// Set a flag so apps can tell the difference, if they need to
966			action.set_onExecute(action.parent.onExecute.fnct);
967			action.execute(clipboard.selected,selected[0]);
968
969			// Clear the clipboard, the files are not there anymore
970			if(action.id.indexOf('move') !== -1)
971			{
972				egw.setSessionItem('phpgwapi', 'egw_clipboard', JSON.stringify({
973					type:[],
974					selected:[]
975				}));
976			}
977		};
978		for(let i = 0; i < actions.length; i++)
979		{
980			_action.getActionById(actions[i].id).onExecute = jQuery.extend(true, {}, _action.onExecute);
981
982			_action.getActionById(actions[i].id).set_onExecute(paste_exec);
983		}
984		return actions.length > 0;
985	}
986
987	/**
988	 * File(s) droped
989	 *
990	 * @param _action
991	 * @param _elems
992	 * @param _target
993	 * @returns
994	 */
995	drop(_action, _elems, _target)
996	{
997		let src = this._elems2paths(_elems);
998
999		// Target will be missing ID if directory is empty
1000		// so start with the current directory
1001		let parent = _action;
1002		let nm = _target ? _target.manager.data.nextmatch : null;
1003		while(!nm && parent.parent)
1004		{
1005			parent = parent.parent;
1006			if(parent.data.nextmatch) nm = parent.data.nextmatch;
1007		}
1008		let nm_dst = this.get_path(nm.getInstanceManager().uniqueId || false);
1009		let dst;
1010		// Action specifies a destination, target does not matter
1011		if(_action.data && _action.data.path)
1012		{
1013			dst = _action.data.path;
1014		}
1015		// File(s) were dropped on a row, they want them inside
1016		else if(_target)
1017		{
1018			dst = '';
1019			let paths = this._elems2paths([_target]);
1020			if(paths[0]) dst = paths[0];
1021
1022			// check if target is a file --> use it's directory instead
1023			if(_target.id)
1024			{
1025				let data = egw.dataGetUIDdata(_target.id);
1026				if(!data || data.data.mime != 'httpd/unix-directory')
1027				{
1028					dst = this.dirname(dst);
1029				}
1030			}
1031		}
1032
1033		// Remember the target for next time
1034		let previous_dsts = jQuery.extend([], egw.preference('drop_history', this.appname));
1035		previous_dsts.unshift(dst);
1036		previous_dsts = Array.from(new Set(previous_dsts)).slice(0, 9);
1037		egw.set_preference(this.appname, 'drop_history', previous_dsts);
1038
1039		// Actual action id will be something like file_drop_{move|copy|link}[_other_id],
1040		// but we need to send move, copy or link
1041		let action_id = _action.id.replace("file_drop_", '').split('_', 1)[0];
1042		this._do_action(action_id, src, false, dst || nm_dst);
1043	}
1044
1045	/**
1046	 * Handle a native / HTML5 file drop from system
1047	 *
1048	 * This is a callback from nextmatch to prevent the default link action, and just upload instead.
1049	 *
1050	 * @param {string} row_uid UID of the row the files were dropped on
1051	 * @param {Files[]} files
1052	 */
1053	filedrop(row_uid, files) : boolean
1054	{
1055		let self = this;
1056		let data = egw.dataGetUIDdata(row_uid);
1057		files = files || window.event.dataTransfer.files;
1058
1059		let path = typeof data != 'undefined' && data.data.mime == "httpd/unix-directory" ? data.data.path : this.get_path();
1060		let widget = this.et2.getWidgetById('upload');
1061
1062		// Override finish to specify a potentially different path
1063		let old_onfinishone = widget.options.onFinishOne;
1064		let old_onfinish = widget.options.onFinish;
1065
1066		widget.options.onFinishOne = function(_event, _file_count) {
1067			self.upload(_event, _file_count, path);
1068		};
1069
1070		widget.options.onFinish = function() {
1071			widget.options.onFinish = old_onfinish;
1072			widget.options.onFinishOne = old_onfinishone;
1073		};
1074		// This triggers the upload
1075		widget.set_value(files);
1076
1077		// Return false to prevent the link
1078		return false;
1079	}
1080
1081	/**
1082	 * Change readonly state for given directory
1083	 *
1084	 * Get call/transported with each get_rows call, but should only by applied to UI if matching curent dir
1085	 *
1086	 * @param {string} _path
1087	 * @param {boolean} _ro
1088	 */
1089	set_readonly(_path, _ro)
1090	{
1091		//alert('set_readonly("'+_path+'", '+_ro+')');
1092		if (!this.path_widget)	// widget not yet ready, try later
1093		{
1094			this.readonly = [_path, _ro];
1095			return;
1096		}
1097		for(let id in this.path_widget)
1098		{
1099			let path = this.get_path(id);
1100
1101			if (_path == path)
1102			{
1103				let ids = ['button[linkpaste]', 'button[paste]', 'button[createdir]', 'button[symlink]', 'upload', 'new'];
1104				for(let i=0; i < ids.length; ++i)
1105				{
1106					let widget = etemplate2.getById(id).widgetContainer.getWidgetById(ids[i]);
1107					if (widget)
1108					{
1109						widget.set_readonly(_ro);
1110					}
1111				}
1112			}
1113		}
1114	}
1115
1116	/**
1117	 * Row or filename in select-file dialog clicked
1118	 *
1119	 * @param {jQuery.event} event
1120	 * @param {et2_widget} widget
1121	 */
1122	select_clicked(event, widget) : boolean
1123	{
1124		if (widget?.value?.is_dir)	// true for "httpd/unix-directory" and "egw/*"
1125		{
1126			let path = null;
1127			// Cannot do this, there are multiple widgets named path
1128			// widget.getRoot().getWidgetById("path");
1129			widget.getRoot().iterateOver(function(widget) {
1130				if(widget.id == "path") path = widget;
1131			},null, et2_textbox);
1132			if(path)
1133			{
1134				path.set_value(widget.value.path);
1135			}
1136		}
1137		else if (this.et2 && this.et2.getArrayMgr('content').getEntry('mode') != 'open-multiple')
1138		{
1139			let editfield = this.et2.getWidgetById('name');
1140			if(editfield)
1141			{
1142				editfield.set_value(widget.value.name);
1143			}
1144		}
1145		else
1146		{
1147			let file = widget.value.name;
1148			widget.getParent().iterateOver(function(widget)
1149			{
1150				if(widget.options.selected_value == file)
1151				{
1152					widget.set_value(widget.get_value() == file ? widget.options.unselected_value : file);
1153				}
1154			}, null, et2_checkbox);
1155		}
1156		// Stop event or it will toggle back off
1157		event.preventDefault();
1158		event.stopPropagation();
1159		return false;
1160	}
1161
1162	/**
1163	 * Set Sudo button's label and change its onclick handler according to its action
1164	 *
1165	 * @param {widget object} _widget sudo buttononly
1166	 * @param {string} _action string of action type {login|logout}
1167	 */
1168	set_sudoButton(_widget, _action: string)
1169	{
1170		let widget = _widget || this.et2.getWidgetById('sudouser');
1171		if (widget)
1172		{
1173			switch (_action)
1174			{
1175				case 'login':
1176					widget.set_label('Logout');
1177					widget.getRoot().getInstanceManager().submit(widget);
1178					break;
1179
1180				default:
1181					widget.set_label('Superuser');
1182					widget.onclick = function(){
1183						jQuery('.superuser').css('display','inline');
1184					};
1185			}
1186		}
1187	}
1188
1189	/**
1190	 * Open file a file dialog from EPL, warn if EPL is not available
1191	 */
1192	fileafile()
1193	{
1194		if (this.egw.user('apps').stylite)
1195		{
1196			this.egw.open_link('/index.php?menuaction=stylite.stylite_filemanager.upload&path='+this.get_path(), '_blank', '670x320');
1197		}
1198		else
1199		{
1200			// This is shown if stylite code is there, but the app is not available
1201			et2_dialog.show_dialog(function(_button)
1202				{
1203					if (_button == et2_dialog.YES_BUTTON) window.open('http://www.egroupware.org/EPL', '_blank');
1204					return true;
1205				}, this.egw.lang('this feature is only available in epl version.')+"\n\n"+
1206				this.egw.lang('You can use regular upload [+] button to upload files.')+"\n\n"+
1207				this.egw.lang('Do you want more information about EPL subscription?'),
1208				this.egw.lang('File a file'), undefined, et2_dialog.BUTTONS_YES_NO, et2_dialog.QUESTION_MESSAGE);
1209		}
1210	}
1211
1212	/**
1213	 * create a share-link for the given entry
1214	 * Overriden from parent to handle empty directories
1215	 *
1216	 * @param {egwAction} _action egw actions
1217	 * @param {egwActionObject[]} _senders selected nm row
1218	 * @param {egwActionObject} _target Drag source.  Not used here.
1219	 * @param {Boolean} _writable Allow edit access from the share.
1220	 * @param {Boolean} _files Allow access to files from the share.
1221	 * @param {Function} _callback Callback with results
1222	 * @returns {Boolean} returns false if not successful
1223	 */
1224	share_link(_action, _senders, _target, _writable, _files, _callback)
1225	{
1226		// Check to see if we're in the empty row (No matches found.) and use current path
1227		let path = _senders[0].id;
1228		if(!path)
1229		{
1230			_senders[0] = {id: this.get_path()};
1231		}
1232		// Pass along any action data
1233		let _extra = {};
1234		for(let i in _action.data)
1235		{
1236			if(i.indexOf('share') == 0)
1237			{
1238				_extra[i] = _action.data[i];
1239			}
1240		}
1241		super.share_link(_action, _senders, _target, _writable, _files, _callback, _extra);
1242	}
1243
1244	/**
1245	 * Share-link callback
1246	 * @param {object} _data
1247	 */
1248	_share_link_callback(_data)
1249	{
1250		if(_data.msg || _data.share_link) window.egw_refresh(_data.msg, this.appname);
1251		console.log("_data", _data);
1252		let app = this;
1253
1254		let copy_link_to_clipboard = function (evt)
1255		{
1256			let $target = jQuery(evt.target);
1257			$target.select();
1258			try
1259			{
1260				let successful = document.execCommand('copy');
1261				if(successful)
1262				{
1263					egw.message(app.egw.lang('Share link copied into clipboard'));
1264					return true;
1265				}
1266			}
1267			catch (e) {}
1268			egw.message('Failed to copy the link!');
1269		};
1270		jQuery("body").on("click", "[name=share_link]", copy_link_to_clipboard);
1271		et2_createWidget("dialog", {
1272			callback: function() {
1273				jQuery("body").off("click", "[name=share_link]", copy_link_to_clipboard);
1274				return true;
1275			},
1276			title: _data.title ? _data.title : (_data.writable || _data.action ==='shareWritableLink' ?
1277					this.egw.lang("Writable share link") : this.egw.lang("Readonly share link")
1278			),
1279			template: _data.template,
1280			width: 450,
1281			value: {content:{ "share_link": _data.share_link }}
1282		});
1283	}
1284
1285	/**
1286	 * Check if a row can have the Hidden Uploads action
1287	 * Needs to be a directory
1288	 */
1289	hidden_upload_enabled(_action: egwAction, _senders: egwActionObject[])
1290	{
1291		if (_senders[0].id == 'nm') return false;
1292		let data = egw.dataGetUIDdata(_senders[0].id);
1293		let readonly = (data?.data.class || '').split(/ +/).indexOf('noEdit') >= 0;
1294
1295		// symlinks dont have mime 'http/unix-directory', but server marks all directories with class 'isDir'
1296		return (!_senders[0].id || data.data.is_dir && !readonly);
1297	}
1298
1299	/**
1300	 * View the link from an existing share
1301	 * (EPL only)
1302	 *
1303	 * @param {egwAction} _action The shareLink action
1304	 * @param {egwActionObject[]} _senders The row clicked on
1305	 */
1306	view_link(_action, _senders) : boolean
1307	{
1308		let id = egw.dataGetUIDdata(_senders[0].id).data.share_id;
1309		egw.json('stylite_filemanager::ajax_view_link', [id],
1310			this._share_link_callback, this, true, this).sendRequest();
1311		return true;
1312	}
1313
1314	/**
1315	 * This function copies the selected file/folder entry as webdav link into clipboard
1316	 *
1317	 * @param {object} _action egw actions
1318	 * @param {object} _senders selected nm row
1319	 * @returns {Boolean} returns false if not successful
1320	 */
1321	copy_link(_action, _senders) : boolean
1322	{
1323		let data = egw.dataGetUIDdata(_senders[0].id);
1324		let url = data ? data.data.download_url : '/webdav.php'+this.id2path(_senders[0].id);
1325		if (url[0] == '/') url = egw.link(url);
1326		if (url.substr(0,4) == 'http'  && url.indexOf('://') <= 5) {
1327			// it's already a full url
1328		}
1329		else
1330		{
1331			let hostUrl = new URL(window.location.href);
1332			url = hostUrl.origin + url;
1333		}
1334
1335		if (url)
1336		{
1337			let elem = jQuery(document.createElement('div'));
1338			let range;
1339			elem.text(url);
1340			elem.appendTo('body');
1341			if (document.selection)
1342			{
1343				range = document.body.createTextRange();
1344				range.moveToElementText(elem);
1345				range.select();
1346			}
1347			else if (window.getSelection)
1348			{
1349				range = document.createRange();
1350				range.selectNode(elem[0]);
1351				window.getSelection().removeAllRanges();
1352				window.getSelection().addRange(range);
1353			}
1354
1355			let successful = false;
1356			try {
1357				successful = document.execCommand('copy');
1358				if (successful)
1359				{
1360					egw.message(this.egw.lang('WebDav link copied into clipboard'));
1361					window.getSelection().removeAllRanges();
1362
1363					return true;
1364				}
1365			}
1366			catch (e) {}
1367			egw.message('Failed to copy the link!');
1368			elem.remove();
1369			return false;
1370		}
1371	}
1372
1373	/**
1374	 * Function to check wheter selected file is editable. ATM only .odt is supported.
1375	 *
1376	 * @param {object} _egwAction egw action object
1377	 * @param {object} _senders object of selected row
1378	 *
1379	 * @returns {boolean} returns true if is editable otherwise false
1380	 */
1381	isEditable(_egwAction, _senders) : boolean
1382	{
1383		if (_senders.length>1) return false;
1384		let data = egw.dataGetUIDdata(_senders[0].id);
1385		let mime = this.et2.getInstanceManager().widgetContainer.getWidgetById('$row');
1386		let fe = egw_get_file_editor_prefered_mimes(data.data.mime);
1387		if (fe && fe.mime && !fe.mime[data.data.mime]) return false;
1388		return !!data.data.mime.match(mime.mime_odf_regex);
1389	}
1390
1391	/**
1392	 * Method to create a new document
1393	 * @param {object} _action either action or node
1394	 * @param {object} _selected either widget or selected row
1395	 *
1396	 * @return {boolean} returns true
1397	 */
1398	create_new(_action, _selected) : boolean
1399	{
1400		let fe = egw.link_get_registry('filemanager-editor');
1401		if (fe && fe["edit"])
1402		{
1403			egw.open_link(egw.link('/index.php', {
1404				menuaction: fe["edit"].menuaction
1405			}), '', fe["popup_edit"]);
1406		}
1407		return true;
1408	}
1409}
1410app.classes.filemanager = filemanagerAPP;
1411