1<?php
2// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project
3//
4// All Rights Reserved. See copyright.txt for details and a complete list of authors.
5// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
6// $Id$
7
8use Tiki\File\PDFHelper;
9
10class Tracker_Field_Files extends Tracker_Field_Abstract implements Tracker_Field_Exportable
11{
12	public static function getTypes()
13	{
14		global $prefs;
15
16		$options = [
17			'FG' => [
18				'name' => tr('Files'),
19				'description' => tr('Attached and upload files stored in the file galleries to the tracker item.'),
20				'prefs' => ['trackerfield_files', 'feature_file_galleries'],
21				'tags' => ['advanced'],
22				'help' => 'Files Tracker Field',
23				'default' => 'y',
24				'params' => [
25					'galleryId' => [
26						'name' => tr('Gallery ID'),
27						'description' => tr('File gallery to upload new files into.'),
28						'filter' => 'int',
29						'legacy_index' => 0,
30						'profile_reference' => 'file_gallery',
31					],
32					'filter' => [
33						'name' => tr('MIME Type Filter'),
34						'description' => tr('Mask for accepted MIME types in the field'),
35						'filter' => 'text',
36						'legacy_index' => 1,
37					],
38					'count' => [
39						'name' => tr('File Count'),
40						'description' => tr('Maximum number of files to be attached on the field.'),
41						'filter' => 'int',
42						'legacy_index' => 2,
43					],
44					'displayMode' => [
45						'name' => tr('Display Mode'),
46						'description' => tr('Show files as object links or via a wiki plugin (img so far)'),
47						'filter' => 'word',
48						'options' => [
49							'' => tr('Links'),
50							'barelink' => tr('Bare Links'),
51							'table' => tr('Table'),
52							'img' => tr('Images'),
53							'vimeo' => tr('Vimeo'),
54							'googleviewer' => tr('Google Viewer'),
55							'moodlescorm' => tr('Moodle Scorm Viewer'),
56						],
57					],
58					'displayParams' => [
59						'name' => tr('Display parameters'),
60						'description' => tr('URL-encoded parameters used such as in the {img} plugin, for example,.') . ' "max=400&desc=namedesc&stylebox=block"',
61						'filter' => 'text',
62					],
63					'displayParamsForLists' => [
64						'name' => tr('Display parameters for lists'),
65						'description' => tr('URL-encoded parameters used such as in the {img} plugin, for example,.') . ' "thumb=box&max=60"',
66						'filter' => 'text',
67					],
68					'displayOrder' => [
69						'name' => tr('Display Order'),
70						'description' => tr('Sort order for the files'),
71						'filter' => 'word',
72						'options' => [
73							'' => tr('Default (order added to tracker item)'),
74							'name_asc' => tr('Name (A - Z)'),
75							'name_desc' => tr('Name (Z - A)'),
76							'filename_asc' => tr('Filename (A - Z)'),
77							'filename_desc' => tr('Filename (Z - A)'),
78							'created_asc' => tr('Created date (old - new)'),
79							'created_desc' => tr('Created date (new - old)'),
80							'lastModif_asc' => tr('Last modified date (old - new)'),
81							'lastModif_desc' => tr('Last modified date (new - old)'),
82							'filesize_asc' => tr('File size (small - large)'),
83							'filesize_desc' => tr('File size (large - small)'),
84							'hits_asc' => tr('Hits (low - high)'),
85							'hits_desc' => tr('Hits (high - low)'),
86						],
87					],
88					'deepGallerySearch' => [
89						'name' => tr('Include Child Galleries'),
90						'description' => tr('Use files from child galleries as well.'),
91						'filter' => 'int',
92						'options' => [
93							0 => tr('No'),
94							1 => tr('Yes'),
95						],
96					],
97					'replace' => [
98						'name' => tr('Replace Existing File'),
99						'description' => tr('Replace the existing file, if any, instead of uploading a new one.'),
100						'filter' => 'alpha',
101						'default' => 'n',
102						'options' => [
103							'n' => tr('No'),
104							'y' => tr('Yes'),
105						],
106					],
107					'browseGalleryId' => [
108						'name' => tr('Browse Gallery ID'),
109						'description' => tr('File gallery browse files. Use 0 for root file gallery. (requires elFinder feature - experimental)') . '. ' . tr('Restrict permissions to view the file gallery to hide the button.') ,
110						'filter' => 'int',
111						'profile_reference' => 'file_gallery',
112					],
113					'duplicateGalleryId' => [
114						'name' => tr('Duplicate Gallery ID'),
115						'description' => tr('File gallery to duplicate files into when copying the tracker item. 0 or empty means do not duplicate (default).'),
116						'filter' => 'int',
117						'profile_reference' => 'file_gallery',
118					],
119					'indexGeometry' => [
120						'name' => tr('Index As Map Layer'),
121						'description' => tr('Index the files in a specific format for use in map searchlayers to display trails and features.'),
122						'filter' => 'text',
123						'default' => '',
124						'options' => [
125							'' => tr('No'),
126							'geojson' => tr('GeoJSON'),
127							'gpx' => tr('GPX'),
128						],
129					],
130					'uploadInModal' => [
131						'name' => tr('Upload In Modal'),
132						'description' => tr('Upload files in a new modal window.'),
133						'filter' => 'alpha',
134						'default' => 'y',
135						'options' => [
136							'n' => tr('No'),
137							'y' => tr('Yes'),
138						],
139					],
140					'image_x' => [
141						'name' => tr('Maximum image width'),
142						'description' => tr('Leave blank to use selected gallery default setting or enter value in pixels to override gallery settings'),
143						'filter' => 'text',
144						'default' => '',
145					],
146					'image_y' => [
147						'name' => tr('Maximum image height'),
148						'description' => tr('Leave blank to use selected gallery default settings or enter value in pixels to override gallery settings'),
149						'filter' => 'text',
150					],
151					'addDecriptionOnUpload' => [
152						'name' => tr('Add Descriptions'),
153						'description' => tr('Add descriptions on uploaded files.'),
154						'filter' => 'alpha',
155						'default' => 'n',
156						'options' => [
157							'n' => tr('No'),
158							'y' => tr('Yes'),
159						],
160					],
161					'requireTitle' => [
162						'name' => tr('Require file title'),
163						'description' => tr('Require a file title which will be saved as the name of the file in the file gallery in addition to the filename. Upload In Modal required.'),
164						'filter' => 'alpha',
165						'default' => 'n',
166						'options' => [
167							'n' => tr('No'),
168							'y' => tr('Yes'),
169						],
170					]
171				],
172			],
173		];
174		if (isset($prefs['vimeo_upload']) && $prefs['vimeo_upload'] === 'y') {
175			$options['FG']['params']['displayMode']['description'] = tr('Show files as object links or via a wiki plugin (img, Vimeo)');
176			$options['FG']['params']['displayMode']['options']['vimeo'] = tr('Vimeo');
177		}
178		return $options;
179	}
180
181	function getFieldData(array $requestData = [])
182	{
183		global $prefs;
184		$filegallib = TikiLib::lib('filegal');
185
186		$galleryId = (int) $this->getOption('galleryId');
187		$count = (int) $this->getOption('count');
188		$deepGallerySearch = (boolean) $this->getOption('deepGallerySearch');
189
190		// to use the user's userfiles gallery enter the fgal_root_user_id which is often (but not always) 2
191		$galleryId = $filegallib->check_user_file_gallery($galleryId);
192
193		$value = '';
194		$ins_id = $this->getInsertId();
195		if (isset($requestData[$ins_id])) {
196			// Incoming data from form
197
198			// Get the list of selected file IDs from the text field
199			$value = $requestData[$ins_id];
200			$fileIds = explode(',', $value);
201
202			// Remove missed uploads
203			$fileIds = array_filter($fileIds);
204
205			// Obtain the info for display and filter by type if specified
206			$fileInfo = $this->getFileInfo($fileIds);
207			$fileInfo = array_filter($fileInfo, [$this, 'filterFile']);
208
209			// Rebuild the database value, but preserve the order the files have been attached to the item
210			foreach ($fileIds as & $fileId) {
211				if (! isset($fileInfo[$fileId])) {
212					$fileId = 0;
213				}
214			}
215
216			// Keep only the last files if a limit is applied
217			if ($count) {
218				$fileIds = array_filter($fileIds);
219				$fileIds = array_slice($fileIds, -$count);
220				$value = implode(',', $fileIds);
221			} else {
222				$value = implode(',', array_filter($fileIds));
223			}
224		} else {
225			$value = $this->getValue();
226
227			// Obtain the information from the database for display
228			$fileIds = array_filter(explode(',', $value));
229			$fileInfo = $this->getFileInfo($fileIds);
230		}
231
232		if ($deepGallerySearch) {
233			$gallery_list = null;
234			$filegallib->getGalleryIds($gallery_list, $galleryId, 'list');
235			$gallery_list = implode(' or ', $gallery_list);
236		} else {
237			$gallery_list = $galleryId;
238		}
239
240		if ($this->getOption('displayMode') == 'img' && $fileIds) {
241			$firstfile = $fileIds[0];
242		} else {
243			$firstfile = 0;
244		}
245
246		$galinfo = $filegallib->get_file_gallery($galleryId);
247		if (! $galinfo) {
248			Feedback::error(tr('Files field: Gallery #%0 not found', $galleryId));
249			return [];
250		}
251		if ($prefs['feature_use_fgal_for_user_files'] !== 'y' || $galinfo['type'] !== 'user') {
252			$perms = Perms::get('file gallery', $galleryId);
253			$canUpload = $perms->upload_files;
254		} else {
255			global $user;
256			$perms = TikiLib::lib('tiki')->get_local_perms($user, $galleryId, 'file gallery', $galinfo, false);		//get_perm_object($galleryId, 'file gallery', $galinfo);
257			$canUpload = $perms['tiki_p_upload_files'] === 'y';
258		}
259
260		$image_x = $this->getOption('image_x');
261		$image_y = $this->getOption('image_y');
262
263		//checking if image_x and image_y are set
264		if (! $image_x) {
265			$image_x = $galinfo['image_max_size_x'];
266		}
267
268		if (! $image_y) {
269			$image_y = $galinfo['image_max_size_y'];
270		}
271
272
273
274		return [
275			'galleryId' => $galleryId,
276			'canUpload' => $canUpload,
277			'limit' => $count,
278			'files' => $fileInfo,
279			'firstfile' => $firstfile,
280			'value' => $value,
281			'filter' => $this->getOption('filter'),
282			'image_x' => $image_x,
283			'image_y' => $image_y,
284			'gallerySearch' => $gallery_list,
285			'requireTitle' => $this->getOption('requireTitle'),
286		];
287	}
288
289	function renderInput($context = [])
290	{
291		global $prefs;
292
293		$context['canBrowse'] = false;
294
295		if ($prefs['fgal_tracker_existing_search'] == 'y') {
296			if ($this->getOption('browseGalleryId')) {
297				$defaultGalleryId = $this->getOption('browseGalleryId');
298			} elseif ($this->getOption('galleryId')) {
299				$defaultGalleryId = $this->getOption('galleryId');
300			} else {
301				$defaultGalleryId = 0;
302			}
303			$deepGallerySearch = $this->getOption('deepGallerySearch');
304			//in case $deepGallerySearch is false
305			$deepGallerySearch = $deepGallerySearch == 1 ? 1 : 0;
306			$image_x = $this->getOption('image_x');
307			$image_y = $this->getOption('image_y');
308
309			if ($prefs['fgal_elfinder_feature'] == 'y') {
310				$smarty = TikiLib::lib('smarty');
311				$smarty->loadPlugin('smarty_function_ticket');
312				$context['onclick'] = 'return openElFinderDialog(this, {
313	defaultGalleryId:' . $defaultGalleryId . ',
314	deepGallerySearch: ' . $deepGallerySearch . ',
315	ticket: \'' . smarty_function_ticket(['mode' => 'get'], $smarty) . '\',
316	getFileCallback: function(file,elfinder){ window.handleFinderFile(file,elfinder); },
317	eventOrigin:this
318});';
319			}
320			$context['galleryId'] = $defaultGalleryId;
321			$context['canBrowse'] = Perms::get(['type' => 'file gallery', 'object' => $defaultGalleryId])->view_file_gallery;
322		}
323
324		return $this->renderTemplate('trackerinput/files.tpl', $context, [
325			'replaceFile' => 'y' == $this->getOption('replace', 'n'),
326			'addDecriptionOnUpload' => $this->getOption('addDecriptionOnUpload') === 'y' ? 1 : 0,
327		]);
328	}
329
330	function renderOutput($context = [])
331	{
332		global $prefs;
333		global $mimetypes;
334		include('lib/mime/mimetypes.php');
335		$galleryId = (int)$this->getOption('galleryId');
336
337		if (! isset($context['list_mode'])) {
338			$context['list_mode'] = 'n';
339		}
340		if (! $this->getOption('displayOrder')) {
341			$value = $this->getValue();
342		} else {
343			$value = $this->getConfiguration('files');
344			$value = implode(',', array_keys($value));
345		}
346
347		if ($context['list_mode'] === 'csv') {
348			return $value;
349		}
350
351		if ($context['list_mode'] === 'text') {
352			return implode(
353				"\n",
354				array_map(
355					function ($file) {
356						return $file['name'];
357					},
358					$this->getConfiguration('files')
359				)
360			);
361		}
362
363		$ret = '';
364		if (empty($value)) {
365			$ret = '&nbsp;';
366		} else {
367			if ($this->getOption('displayMode')) { // images etc
368				$params = [
369					'fileId' => $value,
370				];
371				if ($context['list_mode'] === 'y') {
372					$otherParams = $this->getOption('displayParamsForLists');
373				} else {
374					$otherParams = $this->getOption('displayParams');
375				}
376				if ($otherParams) {
377					parse_str($otherParams, $otherParams);
378					$params = array_merge($params, $otherParams);
379				}
380				$params['fromFieldId'] = $this->getConfiguration('fieldId');
381				$params['fromItemId'] = $this->getItemId();
382				$item = Tracker_Item::fromInfo($this->getItemData());
383				$params['checkItemPerms'] = $item->canModify() ? 'n' : 'y';
384
385				if ($this->getOption('displayMode') == 'img') { // img
386					if ($context['list_mode'] === 'y') {
387						$params['thumb'] = $context['list_mode'];
388						$params['rel'] = 'box[' . $this->getInsertId() . ']';
389					}
390					include_once('lib/wiki-plugins/wikiplugin_img.php');
391					$ret = wikiplugin_img('', $params);
392				} elseif ($this->getOption('displayMode') == 'vimeo') {	// Vimeo videos stored as filegal REMOTEs
393					include_once('lib/wiki-plugins/wikiplugin_vimeo.php');
394					$ret = wikiplugin_vimeo('', $params);
395				} elseif ($this->getOption('displayMode') == 'moodlescorm') {
396					include_once('lib/wiki-plugins/wikiplugin_playscorm.php');
397					foreach ($this->getConfiguration('files') as $fileId => $file) {
398						$params['fileId'] = $fileId;
399						$ret .= wikiplugin_playscorm('', $params);
400					}
401				} elseif ($this->getOption('displayMode') == 'googleviewer') {
402					if ($prefs['auth_token_access'] != 'y') {
403						$ret = tra('Token access needs to be enabled for Google viewer to be used');
404					} else {
405						$files = [];
406						foreach ($this->getConfiguration('files') as $fileId => $file) {
407							global $base_url, $tikiroot, $https_mode;
408							if ($https_mode) {
409								$scheme = 'https';
410							} else {
411								$scheme = 'http';
412							}
413							$googleurl = $scheme . "://docs.google.com/viewer?url=";
414							if ($prefs['feature_sefurl'] === 'y') {
415								$fileurl = urlencode($base_url . "dl" . $fileId);
416							} else {
417								$fileurl = urlencode($base_url . "tiki-download_file.php?fileId=" . $fileId);
418							}
419							require_once 'lib/auth/tokens.php';
420							$tokenlib = AuthTokens::build($prefs);
421							if ($prefs['feature_sefurl'] === 'y') {
422								$token = $tokenlib->createToken(
423									$tikiroot . "dl" . $fileId,
424									[],
425									['Registered'],
426									['timeout' => 600, 'hits' => 6]
427								);
428								$fileurl .= urlencode("?TOKEN=" . $token);
429							} else {
430								$token = $tokenlib->createToken(
431									$tikiroot . "tiki-download_file.php",
432									['fileId' => $fileId],
433									['Registered'],
434									['timeout' => 600, 'hits' => 6]
435								);
436								$fileurl .= urlencode("&TOKEN=" . $token);
437							}
438							$url = $googleurl . $fileurl . '&embedded=true';
439							$title = $file['name'];
440							$files[] = ['url' => $url, 'title' => $title, 'id' => $fileId];
441						}
442						$smarty = TikiLib::lib('smarty');
443						$smarty->assign('files', $files);
444						$ret = $smarty->fetch('trackeroutput/files_googleviewer.tpl');
445					}
446				} elseif ($this->getOption('displayMode') == 'barelink') {
447						$smarty = TikiLib::lib('smarty');
448						$smarty->loadPlugin('smarty_function_object_link');
449						$smarty->loadPlugin('smarty_modifier_sefurl');
450					foreach ($this->getConfiguration('files') as $fileId => $file) {
451						$ret .= smarty_modifier_sefurl($file['fileId'], 'file');
452					}
453				} elseif ($this->getOption('displayMode') == 'table') {
454					$ret = $this->renderTemplate('trackeroutput/files_table.tpl', $context, [
455						'files' => $this->getConfiguration('files')
456					]);
457				}
458				$ret = preg_replace('/~\/?np~/', '', $ret);
459			} else {
460				$smarty = TikiLib::lib('smarty');
461				$smarty->loadPlugin('smarty_function_object_link');
462				$smarty->loadPlugin('smarty_modifier_iconify');
463				$ret = '<ol class="tracker-item-files">';
464
465				foreach ($this->getConfiguration('files') as $fileId => $file) {
466					$ret .= '<li class="m-1">';
467					if ($prefs['vimeo_upload'] == 'y' && $this->getOption('displayMode') == 'vimeo') {
468						$ret .= smarty_function_icon(['name' => 'vimeo'], $smarty->getEmptyInternalTemplate());
469					} else {
470						$ret .= smarty_modifier_iconify('tiki-download_file.php?fileId=' . $fileId, $file['filetype'], $fileId, 2);
471					}
472
473					$ret .= smarty_function_object_link(['type' => 'file', 'id' => $fileId, 'title' => $file['name']], $smarty->getEmptyInternalTemplate());
474
475					$globalperms = Perms::get([ 'type' => 'file gallery', 'object' => $galleryId ]);
476
477					if ($prefs['feature_draw'] == 'y' &&
478						$globalperms->upload_files == 'y' &&
479						($file['filetype'] == $mimetypes["svg"] ||
480						$file['filetype'] == $mimetypes["gif"] ||
481						$file['filetype'] == $mimetypes["jpg"] ||
482						$file['filetype'] == $mimetypes["png"] ||
483						$file['filetype'] == $mimetypes["tiff"])
484					) {
485						$smarty->loadPlugin('smarty_function_icon');
486						$editicon = smarty_function_icon(['name' => 'edit'], $smarty->getEmptyInternalTemplate());
487						$ret .= " <a href='tiki-edit_draw.php?fileId=" . $file['fileId']
488							. "' onclick='return $(this).ajaxEditDraw();' class='tips' title='Edit: " . $file['name']
489							. "' data-fileid='" . $file['fileId'] . "' data-galleryid='" . $galleryId . "'>
490							$editicon
491						</a>";
492					}
493
494					$smarty->loadPlugin('smarty_function_icon');
495					$viewicon = smarty_function_icon(['name' => 'view'], $smarty->getEmptyInternalTemplate());
496
497					if ($prefs['fgal_pdfjs_feature'] == 'y' &&
498						($file['filetype'] == $mimetypes["pdf"] || (PDFHelper::canConvertToPDF($file['filetype']) && $prefs['fgal_convert_documents_pdf'] == 'y'))
499					) {
500						$ret .= " <a href='tiki-display.php?fileId=" . $file['fileId']
501							. "' target='_blank' class='tips' title='Preview: " . $file['filename'] . "'>
502							$viewicon
503						</a>";
504					} elseif (strpos($file['filetype'], 'video/') === 0) {
505						$src = smarty_modifier_sefurl($file['fileId'], 'display');
506
507						$ret .= " <a href='$src' target='_blank' class='tips' title='Preview: " . $file['filename'] . "' data-box='box-type=video'>
508							$viewicon
509						</a>";
510					} else {
511						$dataAttributes = [];
512
513						if ($file['filetype'] === 'text/plain') {
514							$dataAttributes[] = 'data-is-text="1"';
515						}
516
517						$src = smarty_modifier_sefurl($file['fileId'], 'display');
518						$ret .= " <a href='" . $src . "' target='_blank' class='tips' title='Preview: " . $file['filename'] . "' data-box='box-" . $this->getConfiguration('fieldId') . "' " . implode(' ', $dataAttributes) . ">
519							$viewicon
520						</a>";
521					}
522
523					$ret .= '</li>';
524				}
525				$ret .= '</ol>';
526			}
527		}
528		return $ret;
529	}
530
531	function handleSave($value, $oldValue)
532	{
533		$new = array_diff(explode(',', $value), explode(',', $oldValue));
534		$remove = array_diff(explode(',', $oldValue), explode(',', $value));
535
536		$itemId = $this->getItemId();
537
538		if ($itemId) {
539			$relationlib = TikiLib::lib('relation');
540			$relations = $relationlib->get_relations_from('trackeritem', $itemId, 'tiki.file.attach');
541			foreach ($relations as $existing) {
542				if ($existing['type'] != 'file') {
543					continue;
544				}
545
546				if (in_array($existing['itemId'], $remove)) {
547					$relationlib->remove_relation($existing['relationId']);
548				}
549			}
550
551			foreach ($new as $fileId) {
552				if (! empty($fileId)) {
553					$relationlib->add_relation('tiki.file.attach', 'trackeritem', $itemId, 'file', $fileId);
554				}
555			}
556		}
557
558		return [
559			'value' => $value,
560		];
561	}
562
563	/**
564	 * called from action_clone_item and duplicates the related files if option duplicateGalleryID is set
565	 */
566	function handleClone()
567	{
568		global $prefs;
569
570		$oldValue = $this->getValue();
571		if ($galleryId = $this->getOption('duplicateGalleryId')) {
572			$filegallib = TikiLib::lib('filegal');
573
574			// to use the user's userfiles gallery enter the fgal_root_user_id which is often (but not always) 2
575			$galleryId = $filegallib->check_user_file_gallery($galleryId);
576
577			$newIds = [];
578
579			foreach (array_filter(explode(',', $oldValue)) as $fileId) {
580				$newIds[] = $filegallib->duplicate_file($fileId, $galleryId);
581			}
582
583			return $this->handleSave(implode(',', $newIds), $oldValue);
584		}
585		return [
586			'value' => $oldValue,
587		];
588	}
589
590	function watchCompare($old, $new)
591	{
592		$name = $this->getConfiguration('name');
593		$isVisible = $this->getConfiguration('isHidden', 'n') == 'n';
594
595		if (! $isVisible) {
596			return;
597		}
598
599		$filegallib = TikiLib::lib('filegal');
600
601		$oldFileIds = explode(',', $old);
602		$newFileIds = explode(',', $new);
603
604		$oldFileInfos = empty($oldFileIds) ? [] : $filegallib->get_files_info(null, $oldFileIds);
605		$newFileInfos = empty($newFileIds) ? [] : $filegallib->get_files_info(null, $newFileIds);
606
607		$oldValueLines = '';
608		foreach ($oldFileInfos as $info) {
609			$oldValueLines .= '> ' . $info['filename'];
610		}
611		$newValueLines = '';
612		foreach ($newFileInfos as $info) {
613			$newValueLines .= '> ' . $info['filename'];
614		}
615
616		return "[-[$name]-]:\n--[Old]--:\n$oldValueLines\n\n*-[New]-*:\n$newValueLines";
617	}
618
619	public function renderDiff($context = [])
620	{
621		$smarty = TikiLib::lib('smarty');
622		$smarty->loadPlugin('smarty_modifier_sefurl');
623		$smarty->loadPlugin('smarty_modifier_escape');
624		$smarty->loadPlugin('smarty_modifier_iconify');
625
626		if ($context['oldValue']) {
627			$old = $context['oldValue'];
628		} else {
629			$old = '';
630		}
631		if ($context['value']) {
632			$new = $context['value'];
633		} else {
634			$new = $this->getValue('');
635		}
636		if (empty($context['diff_style'])) {
637			$context['diff_style'] = 'sidediff';
638		}
639
640		$filegallib = TikiLib::lib('filegal');
641
642		$oldFileIds = explode(',', $old);
643		$newFileIds = explode(',', $new);
644
645		$filesRemoved = array_diff($oldFileIds, $newFileIds);
646		$filesAdded = array_diff($newFileIds, $oldFileIds);
647
648		$addedFileInfos = empty($filesAdded) ? [] : $filegallib->get_files_info(null, $filesAdded);
649		$removedFileInfos = empty($filesRemoved) ? [] : $filegallib->get_files_info(null, $filesRemoved);
650
651		$result = '<table class="table"><tr><td class="diffdeleted">-</td><td class="diffdeleted"><del class="diffchar deleted">';
652
653		foreach ($removedFileInfos as $file) {
654			$url = smarty_modifier_sefurl($file['fileId'], 'file');
655			$result .= smarty_modifier_iconify($url, $file['filetype'], $file['fileId'], 1);
656			$result .= ' <a href="' . $url . '">' . smarty_modifier_escape($file['name']) . '</a><br>';
657		}
658
659		$result .= '</del></td><td class="diffadded">+</td><td class="diffadded"><ins class="diffchar inserted">';
660
661		foreach ($addedFileInfos as $file) {
662			$url = smarty_modifier_sefurl($file['fileId'], 'file');
663			$result .= smarty_modifier_iconify($url, $file['filetype'], $file['fileId'], 1);
664			$result .= ' <a href="' . $url . '">' . smarty_modifier_escape($file['name']) . '</a><br>';
665		}
666
667		$result .= '</ins></td></tr></table>';
668
669		return $result;
670	}
671
672	function filterFile($info)
673	{
674		$filter = $this->getOption('filter');
675
676		if (! $filter) {
677			return true;
678		}
679
680		$parts = explode('*', $filter);
681		$parts = array_map('preg_quote', $parts, array_fill(0, count($parts), '/'));
682
683		$body = implode('[\w-]*', $parts);
684
685		// Force begin, ignore end which may contain charsets or other attributes
686		return preg_match("/^$body/", $info['filetype']);
687	}
688
689	private function getFileInfo($ids)
690	{
691		$db = TikiDb::get();
692		$table = $db->table('tiki_files');
693
694		$sortOrder = $this->getOption('displayOrder');
695
696		$data = $table->fetchAll(
697			[
698				'fileId',
699				'name',
700				'filename',
701				'filetype',
702				'archiveId',
703				'lastModif',
704				'description'
705			],
706			[
707				'fileId' => $table->in($ids),
708			],
709			-1,
710			-1,
711			$table->sortMode($sortOrder)
712		);
713
714		$out = [];
715		foreach ($data as $info) {
716			$out[$info['fileId']] = $info;
717		}
718
719		if (! $sortOrder) {	// re-order result into order they were attached
720			$out2 = [];
721			foreach ($ids as $id) {
722				if (isset($out[$id])) {
723					$out2["$id"] = $out[$id];
724				} else {
725					$itemId = $this->getItemId();
726					if ($itemId) {
727						$smarty = TikiLib::lib('smarty');
728						$smarty->loadPlugin('smarty_function_object_link');
729
730						Feedback::warning(
731							tr(
732								'File #%0 missing (was attached to trackerfield "%1" on item %2)',
733								$id,
734								$this->getConfiguration('permName'),
735								smarty_function_object_link(
736									[
737										'id'   => $itemId,
738										'type' => 'trackeritem',
739									],
740									$smarty
741								)
742							)
743						);
744					}
745				}
746			}
747			$out = $out2;
748		} elseif (strstr($sortOrder, 'name')) {
749			$sep = strrpos($sortOrder, '_');
750			$field = substr($sortOrder, 0, $sep);
751			$dir = substr($sortOrder, $sep + 1);
752			$sortArray = array_map(function ($file) use ($field) {
753				return isset($file[$field]) ? $file[$field] : '';
754			}, $out);
755			natsort($sortArray);
756			if ($dir == 'desc') {
757				$sortArray = array_reverse($sortArray, true);
758			}
759			$sorted = [];
760			foreach ($sortArray as $key => $_) {
761				$sorted[$key] = $out[$key];
762			}
763			$out = $sorted;
764		}
765
766		return $out;
767	}
768
769	private function handleUpload($galleryId, $file)
770	{
771		if (empty($file['tmp_name'])) {
772			// Not an actual file upload attempt, just skip
773			return false;
774		}
775
776		if (! is_uploaded_file($file['tmp_name'])) {
777			Feedback::error(tr('Problem with uploaded file: "%0"', $file['name']));
778			return false;
779		}
780
781		$filegallib = TikiLib::lib('filegal');
782		$gal_info = $filegallib->get_file_gallery_info($galleryId);
783
784		if (! $gal_info) {
785			Feedback::error(tr('No gallery for uploaded file, galleryId=%0', $galleryId));
786			return false;
787		}
788
789		$perms = Perms::get('file gallery', $galleryId);
790		if (! $perms->upload_files) {
791			Feedback::error(tr('You don\'t have permission to upload a file to gallery "%0"', $gal_info['name']));
792			return false;
793		}
794
795		$fileIds = $this->getConfiguration('files');
796
797		if ($this->getOption('displayMode') == 'img' && is_array($fileIds) && count($fileIds) > 0) {
798			return $filegallib->update_single_file($gal_info, $file['name'], $file['size'], $file['type'], file_get_contents($file['tmp_name']), $fileIds[0]);
799		} else {
800			return $filegallib->upload_single_file($gal_info, $file['name'], $file['size'], $file['type'], file_get_contents($file['tmp_name']));
801		}
802	}
803
804	function getDocumentPart(Search_Type_Factory_Interface $typeFactory)
805	{
806		if ($this->getOption('indexGeometry') && $this->getValue()) {
807			TikiLib::lib('smarty')->loadPlugin('smarty_modifier_sefurl');
808			$urls = [];
809
810			foreach (explode(',', $this->getValue()) as $value) {
811				$urls[] = smarty_modifier_sefurl($value, 'file');
812			}
813			return [
814				'geo_located' => $typeFactory->identifier('y'),
815				'geo_file' => $typeFactory->identifier(implode(',', $urls)),
816				'geo_file_format' => $typeFactory->identifier($this->getOption('indexGeometry')),
817			];
818		} else {
819			return parent::getDocumentPart($typeFactory);
820		}
821	}
822
823	function getProvidedFields()
824	{
825		if ($this->getOption('indexGeometry') && $this->getValue()) {
826			return ['geo_located', 'geo_file', 'geo_file_format'];
827		} else {
828			return parent::getProvidedFields();
829		}
830	}
831
832	function getGlobalFields()
833	{
834		if ($this->getOption('indexGeometry') && $this->getValue()) {
835			return [];
836		} else {
837			return parent::getGlobalFields();
838		}
839	}
840
841	function getTabularSchema()
842	{
843		$schema = new Tracker\Tabular\Schema($this->getTrackerDefinition());
844
845		$permName = $this->getConfiguration('permName');
846		$name = $this->getConfiguration('name');
847
848		$schema->addNew($permName, 'default')
849			->setLabel($name)
850			->setRenderTransform(function ($value) {
851				return $value;
852			})
853			->setParseIntoTransform(function (&$info, $value) use ($permName) {
854				$info['fields'][$permName] = $value;
855			});
856
857		return $schema;
858	}
859}
860