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
8include_once "vendor_bundled/vendor/studio-42/elfinder/php/elFinderConnector.class.php";
9include_once "vendor_bundled/vendor/studio-42/elfinder/php/elFinder.class.php";
10include_once "vendor_bundled/vendor/studio-42/elfinder/php/elFinderVolumeDriver.class.php";
11
12include_once 'lib/jquery_tiki/elfinder/elFinderVolumeTikiFiles.class.php';
13include_once 'lib/jquery_tiki/elfinder/tikiElFinder.php';
14
15class Services_File_FinderController
16{
17	private $fileController;
18
19	private $parentIds;
20
21	function setUp()
22	{
23		global $prefs;
24
25		if ($prefs['feature_file_galleries'] != 'y') {
26			throw new Services_Exception_Disabled('feature_file_galleries');
27		}
28		if ($prefs['fgal_elfinder_feature'] != 'y') {
29			throw new Services_Exception_Disabled('fgal_elfinder_feature');
30		}
31		$this->fileController = new Services_File_Controller();
32		$this->fileController->setUp();
33
34		$this->parentIds = null;
35	}
36
37	/**********************
38	 * elFinder functions *
39	 *********************/
40
41	/***
42	 * Main "connector" to handle all requests from elfinder
43	 *
44	 * @param $input
45	 *
46	 * @return array
47	 * @throws Exception
48	 */
49
50	public function action_finder($input)
51	{
52		global $prefs, $user;
53
54
55		if ($this->parentIds === null) {
56			$ids = TikiLib::lib('filegal')->getGalleriesParentIds();
57			$this->parentIds = [ 'galleries' => [], 'files' => [] ];
58			foreach ($ids as $id) {
59				if ($id['parentId'] > 0) {
60					$this->parentIds['galleries'][(int) $id['galleryId']] = (int) $id['parentId'];
61				}
62			}
63			$tiki_files = TikiDb::get()->table('tiki_files');
64			$this->parentIds['files'] = array_map('intval', $tiki_files->fetchMap('fileId', 'galleryId', []));
65		}
66
67		// turn off some elfinder commands here too (stops the back-end methods being accessible)
68		$disabled = ['mkfile', 'edit', 'archive', 'resize'];
69		// done so far: 'rename', 'rm', 'duplicate', 'upload', 'copy', 'cut', 'paste', 'mkdir', 'extract',
70
71		// check for a "userfiles" gallery - currently although elFinder can support more than one root, it always starts in the first one
72		$opts = [
73			'debug' => ($prefs['fgal_elfinder_debug'] === 'y'),
74			'roots' => [],
75			'bind'  => [
76				//check csrf prior to executing state-changing actions
77				'duplicate.pre mkdir.pre paste.pre rename.pre rm.pre upload.pre' => [
78					[$this, 'csrfCheck']
79				]
80			]
81		];
82
83		$rootDefaults = [
84			'driver' => 'TikiFiles', // driver for accessing file system (REQUIRED)
85//			'path' => $rootId, // tiki root filegal - path to files (REQUIRED) - to be filled in later
86			'disabled' => $disabled,
87//			'URL'           => 									// URL to files (seems not to be REQUIRED)
88			'accessControl' => [$this, 'elFinderAccess'], // obey tiki perms
89			'uploadMaxSize' => ini_get('upload_max_filesize'),
90			'accessControlData' => [
91				'deepGallerySearch' => $input->deepGallerySearch->int(),
92				'parentIds' => $this->parentIds,
93			],
94			'alias' => tr('Default Root Gallery'),	// just in case
95		];
96
97		// gallery to start in
98		$startGallery = $input->defaultGalleryId->int();
99
100		if ($startGallery) {
101			$gal_info = TikiLib::lib('filegal')->get_file_gallery_info($startGallery);
102			if (! $gal_info) {
103				Feedback::error(tr('Gallery ID %0 not found', $startGallery));
104				$startGallery = $prefs['fgal_root_id'];
105			}
106		}
107
108		// 'startPath' not functioning with multiple roots as yet (https://github.com/Studio-42/elFinder/issues/351)
109		// so work around it for now with startRoot
110
111		$opts['roots'][] = array_merge(
112			// normal file gals
113			$rootDefaults,
114			[
115				'path' => $prefs['fgal_root_id'],		// should be a function?
116				'alias' => tr('File Galleries'),
117			]
118		);
119		$startRoot = 0;
120
121		if (! empty($user) && $prefs['feature_userfiles'] == 'y' && $prefs['feature_use_fgal_for_user_files'] == 'y') {
122			if ($startGallery && $startGallery == $prefs['fgal_root_user_id'] && ! Perms::get('file gallery', $startGallery)->admin_file_galleries) {
123				$startGallery = (int) TikiLib::lib('filegal')->get_user_file_gallery();
124			}
125			$userRootId = $prefs['fgal_root_user_id'];
126
127			if ($startGallery != $userRootId) {
128				$gal_info = TikiLib::lib('filegal')->get_file_gallery_info($startGallery);
129				if ($gal_info['type'] == 'user') {
130					$startRoot = count($opts['roots']);
131				}
132			} else {
133				$startRoot = count($opts['roots']);
134			}
135			$opts['roots'][] = array_merge(
136				$rootDefaults,
137				[
138					'path' => $userRootId,		// should be $prefs['fgal_root_id']?
139					'alias' => tr('Users File Galleries'),
140				]
141			);
142		}
143
144		if ($prefs['feature_wiki_attachments'] == 'y' && $prefs['feature_use_fgal_for_wiki_attachments'] === 'y') {
145			if ($startGallery && $startGallery == $prefs['fgal_root_wiki_attachments_id']) {
146				$startRoot = count($opts['roots']);
147			}
148			$opts['roots'][] = array_merge(
149				$rootDefaults,
150				[
151					'path' => $prefs['fgal_root_wiki_attachments_id'],		// should be $prefs['fgal_root_id']?
152					'alias' => tr('Wiki Attachments'),
153				]
154			);
155		}
156
157		if ($startGallery) {
158			$opts['startRoot'] = $startRoot;
159			$d = $opts['roots'][$startRoot]['path'] == $startGallery ? '' : 'd_';	// needs to be the cached name in elfinder (with 'd_' in front) unless it's the root id
160			$opts['roots'][$startRoot]['startPath'] = $d . $startGallery;
161		}
162
163/* thumb size not working due to css issues - tried this in setup/javascript.php but needs extensive css overhaul to get looking right
164		if ($prefs['fgal_elfinder_feature'] === 'y') {
165			$tmbSize = (int) $prefs['fgal_thumb_max_size'] / 2;
166			TikiLib::lib('header')->add_css(".elfinder-cwd-icon {width:{$tmbSize}px; height:{$tmbSize}px;}");	// def 48
167			$tmbSize += 4;	// def 52
168			TikiLib::lib('header')->add_css(".elfinder-cwd-view-icons .elfinder-cwd-file-wrapper {width:{$tmbSize}px; height:{$tmbSize}px;}");
169			$tmbSize += 28; $tmbSizeW = $tmbSize + 40;	// def 120 x 80
170			TikiLib::lib('header')->add_css(".elfinder-cwd-view-icons .elfinder-cwd-file {width: {$tmbSizeW}px;height: {$tmbSize}px;}");
171		}
172*/
173		// run elFinder
174
175		session_write_close();
176
177		$elFinder = new tikiElFinder($opts);
178		$connector = new elFinderConnector($elFinder);
179
180		$filegallib = TikiLib::lib('filegal');
181		if ($input->cmd->text() === 'tikiFileFromHash') {	// intercept tiki only commands
182			$fileId = $elFinder->realpath($input->hash->text());
183			if (strpos($fileId, 'f_') !== false) {
184				$info = $filegallib->get_file(str_replace('f_', '', $fileId));
185			} else {
186				$info = $filegallib->get_file_gallery(str_replace('d_', '', $fileId));
187			}
188			$params = [];
189			if ($input->filegals_manager->text()) {
190				$params['filegals_manager'] = $input->filegals_manager->text();
191			}
192			if ($input->insertion_syntax->text()) {
193				$params['insertion_syntax'] = $input->insertion_syntax->text();
194			}
195			$info['wiki_syntax'] = $filegallib->getWikiSyntax($info['galleryId'], empty($info['fileId']) ? [] : $info, $params);
196			$info['data'] = '';	// binary data makes JSON fall over
197			return $info;
198		} elseif ($input->cmd->text() === 'file') {
199			// intercept download command and use tiki-download_file so the mime type and extension is correct
200			$fileId = $elFinder->realpath($input->target->text());
201			if (strpos($fileId, 'f_') !== false) {
202				global $base_url;
203
204				$fileId = str_replace('f_', '', $fileId);
205				$display = '';
206
207				$url = $base_url . 'tiki-download_file.php?fileId=' . $fileId;
208
209				if (! $input->download->int()) {	// images can be displayed
210					$info = $filegallib->get_file($fileId);
211
212					if (strpos($info['filetype'], 'image/') !== false) {
213						$url .= '&display';
214					} elseif ($prefs['fgal_viewerjs_feature'] === 'y' &&
215							($info['filetype'] === 'application/pdf' or
216									strpos($info['filetype'], 'application/vnd.oasis.opendocument.') !== false)) {
217						$url = TikiLib::lib('access')->absoluteUrl($prefs['fgal_viewerjs_uri'] . '#' . $url);
218					}
219				}
220
221				TikiLib::lib('access')->redirect($url);
222				return [];
223			}
224		}
225
226		// elfinder needs "raw" $_GET or $_POST
227		if ($_SERVER["REQUEST_METHOD"] == 'POST') {
228			$_POST = $input->asArray();
229		} else {
230			$_GET = $input->asArray();
231			TikiLib::lib('access')->setTicket();
232		}
233
234		$connector->run();
235		// deals with response
236
237		return [];
238	}
239
240	/**
241	 * elFinderAccess "accessControl" callback.
242	 *
243	 * @param  string $attr attribute name (read|write|locked|hidden)
244	 * @param  string $path file path relative to volume root directory started with directory separator
245	 * @param         $data
246	 * @param         $volume
247	 *
248	 * @return bool|null
249	 * @throws Exception
250	 */
251	function elFinderAccess($attr, $path, $data, $volume)
252	{
253		global $prefs;
254
255		$ar = explode('_', $path);
256		$visible = true;		// for now
257		if (count($ar) === 2) {
258			$isgal = $ar[0] === 'd';
259			$id = $ar[1];
260			if ($isgal) {
261				$visible = $this->isVisible($id, $data, $isgal);
262			} else {
263				$visible = $this->isVisible($this->parentIds['files'][$id], $data, $isgal);
264			}
265		} else {
266			$isgal = true;
267			$id = $path;
268		}
269
270		if ($isgal) {
271			$perms = TikiLib::lib('tiki')->get_perm_object($id, 'file gallery', TikiLib::lib('filegal')->get_file_gallery_info($id));
272		} else {
273			$perms = TikiLib::lib('tiki')->get_perm_object($id, 'file', TikiLib::lib('filegal')->get_file($id));
274		}
275
276		$perms = array_merge([
277			'tiki_p_admin_file_galleries'     => 'n',
278			'tiki_p_download_files'           => 'n',
279			'tiki_p_upload_files'             => 'n',
280			'tiki_p_view_file_gallery'        => 'n',
281			'tiki_p_remove_files'             => 'n',
282			'tiki_p_create_file_galleries'    => 'n',
283			'tiki_p_edit_gallery_file'        => 'n',
284			'tiki_p_list_file_galleries'      => 'n',
285			'tiki_p_assign_perm_file_gallery' => 'n',
286			'tiki_p_batch_upload_file_dir'    => 'n',
287			'tiki_p_batch_upload_files'       => 'n',
288			'tiki_p_view_fgal_explorer'       => 'n',
289			'tiki_p_view_fgal_path'           => 'n',
290			'tiki_p_upload_javascript'        => 'n',
291			'tiki_p_upload_svg'               => 'n',
292		], $perms);
293
294		switch ($attr) {
295			case 'read':
296				if ($isgal) {
297					return $visible && ($perms['tiki_p_view_file_gallery'] === 'y' || $id == $prefs['fgal_root_id']);
298				} else {
299					return $visible && $perms['tiki_p_download_files'] === 'y';
300				}
301			case 'write':
302				if ($isgal) {
303					return $visible && ($perms['tiki_p_admin_file_galleries'] === 'y' || $perms['tiki_p_upload_files'] === 'y');
304				} else {
305					return $visible && ($perms['tiki_p_edit_gallery_file'] === 'y' || $perms['tiki_p_remove_files'] === 'y');
306				}
307			case 'locked':
308			case 'hidden':
309				return ! $visible;
310			default:
311				return false;
312		}
313	}
314
315	private function isVisible($id, $data, $isgal)
316	{
317		$visible = true;
318
319		if (! empty($data['startPath'])) {
320			if ($data['startPath'] == $id) { // is startPath
321				$visible = true;
322				return $visible;
323			} else {
324				$isParentOf = $this->isParentOf($id, $data['startPath'], $this->parentIds['galleries']);
325
326				if (isset($data['deepGallerySearch']) && $data['deepGallerySearch'] == 0) { // not startPath and not deep
327					if ($isParentOf && $isgal) {
328						$visible = true;
329						return $visible;
330					} else {
331						$visible = false;
332						return $visible;
333					}
334				} else {
335					if ($isParentOf && $isgal) {
336						$visible = true;
337					} else {
338						$visible = false;
339					}
340					$pid = $this->parentIds['galleries'][$id];
341					while ($pid) {
342						if ($pid == $data['startPath']) {
343							$visible = true;
344							break;
345						}
346						$pid = $this->parentIds['galleries'][$pid];
347					}
348					return $visible;
349				}
350			}
351		}
352		return $visible;
353	}
354
355	private function isParentOf($id, $child, $parentIds)
356	{
357		if (! isset($parentIds[$child])) {
358			return false;
359		} elseif ($parentIds[$child] == $id) {
360			return true;
361		} else {
362			return $this->isParentOf($child, $parentIds[$child], $parentIds);
363		}
364	}
365
366	/**
367	 * Anti-CSRF check. To be run pre-execution of commands that change the database
368	 *
369	 * @param $cmd
370	 * @param $args
371	 * @param $elfinder tikiElFinder
372	 * @param $volume   elFinderVolumeTikiFiles
373	 *
374	 * @return mixed $results array
375	 * @throws Services_Exception
376	 */
377	public function csrfCheck($cmd, &$args, $elfinder, $volume)
378	{
379		$access = TikiLib::lib('access');
380		//don't unset ticket since multiple actions may be performed without refreshing the page
381		if ($access->checkCsrf('none', false)) {
382			$access->setTicket();
383			$elfinder->setCustomData('ticket', $access->getTicket());
384		} else {
385			return  [
386				'preventexec' => true,
387				'results' => [
388					'error' => tr('Potential cross-site request forgery (CSRF) detected. Operation blocked. Reloading the page may help.')
389				]
390			];
391		}
392	}
393}