1<?php
2/**
3 * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
4 *
5 * @license GNU AGPL version 3 or any later version
6 *
7 * This program is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU Affero General Public License as
9 * published by the Free Software Foundation, either version 3 of the
10 * License, or (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 * GNU Affero General Public License for more details.
16 *
17 * You should have received a copy of the GNU Affero General Public License
18 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 *
20 */
21
22namespace OCA\GroupFolders\Folder;
23
24use OC\Files\Cache\Cache;
25use OC\Files\Node\Node;
26use OCA\GroupFolders\Mount\GroupFolderStorage;
27use OCA\GroupFolders\Mount\GroupMountPoint;
28use OCP\Constants;
29use OCP\DB\Exception;
30use OCP\DB\QueryBuilder\IQueryBuilder;
31use OCP\Files\Cache\ICacheEntry;
32use OCP\Files\IMimeTypeLoader;
33use OCP\Files\IRootFolder;
34use OCP\IDBConnection;
35use OCP\IGroupManager;
36use OCP\IUser;
37use OCP\IUserManager;
38
39class FolderManager {
40	/** @var IDBConnection */
41	private $connection;
42
43	/** @var IGroupManager */
44	private $groupManager;
45
46	/** @var IMimeTypeLoader */
47	private $mimeTypeLoader;
48
49	public function __construct(IDBConnection $connection, IGroupManager $groupManager = null, IMimeTypeLoader $mimeTypeLoader = null) {
50		$this->connection = $connection;
51
52		// files_fulltextsearch compatibility
53		if (!$groupManager) {
54			$groupManager = \OC::$server->get(IGroupManager::class);
55		}
56		if (!$mimeTypeLoader) {
57			$mimeTypeLoader = \OC::$server->get(IMimeTypeLoader::class);
58		}
59		$this->groupManager = $groupManager;
60		$this->mimeTypeLoader = $mimeTypeLoader;
61	}
62
63	/**
64	 * @return (array|bool|int|mixed)[][]
65	 *
66	 * @psalm-return array<int, array{id: int, mount_point: mixed, groups: array<empty, empty>|array<array-key, int>, quota: int, size: int, acl: bool}>
67	 * @throws Exception
68	 */
69	public function getAllFolders(): array {
70		$applicableMap = $this->getAllApplicable();
71
72		$query = $this->connection->getQueryBuilder();
73
74		$query->select('folder_id', 'mount_point', 'quota', 'acl')
75			->from('group_folders', 'f');
76
77		$rows = $query->executeQuery()->fetchAll();
78
79		$folderMap = [];
80		foreach ($rows as $row) {
81			$id = (int)$row['folder_id'];
82			$folderMap[$id] = [
83				'id' => $id,
84				'mount_point' => $row['mount_point'],
85				'groups' => $applicableMap[$id] ?? [],
86				'quota' => (int)$row['quota'],
87				'size' => 0,
88				'acl' => (bool)$row['acl']
89			];
90		}
91
92		return $folderMap;
93	}
94
95	/**
96	 * @throws Exception
97	 */
98	private function getGroupFolderRootId(int $rootStorageId): int {
99		$query = $this->connection->getQueryBuilder();
100
101		$query->select('fileid')
102			->from('filecache')
103			->where($query->expr()->eq('storage', $query->createNamedParameter($rootStorageId)))
104			->andWhere($query->expr()->eq('path_hash', $query->createNamedParameter(md5('__groupfolders'))));
105
106		return (int)$query->executeQuery()->fetchOne();
107	}
108
109	private function joinQueryWithFileCache(IQueryBuilder $query, int $rootStorageId): void {
110		$query->leftJoin('f', 'filecache', 'c', $query->expr()->andX(
111			// concat with empty string to work around missing cast to string
112			$query->expr()->eq('name', $query->func()->concat('f.folder_id', $query->expr()->literal(""))),
113			$query->expr()->eq('parent', $query->createNamedParameter($this->getGroupFolderRootId($rootStorageId)))
114		));
115	}
116
117	/**
118	 * @return (array|bool|int|mixed)[][]
119	 *
120	 * @psalm-return array<int, array{id: int, mount_point: mixed, groups: array<empty, empty>|array<array-key, int>, quota: int, size: int, acl: bool, manage: mixed}>
121	 * @throws Exception
122	 */
123	public function getAllFoldersWithSize(int $rootStorageId): array {
124		$applicableMap = $this->getAllApplicable();
125
126		$query = $this->connection->getQueryBuilder();
127
128		$query->select('folder_id', 'mount_point', 'quota', 'size', 'acl')
129			->from('group_folders', 'f');
130		$this->joinQueryWithFileCache($query, $rootStorageId);
131
132		$rows = $query->executeQuery()->fetchAll();
133
134		$folderMappings = $this->getAllFolderMappings();
135
136		$folderMap = [];
137		foreach ($rows as $row) {
138			$id = (int)$row['folder_id'];
139			$mappings = $folderMappings[$id] ?? [];
140			$folderMap[$id] = [
141				'id' => $id,
142				'mount_point' => $row['mount_point'],
143				'groups' => $applicableMap[$id] ?? [],
144				'quota' => (int)$row['quota'],
145				'size' => $row['size'] ? (int)$row['size'] : 0,
146				'acl' => (bool)$row['acl'],
147				'manage' => $this->getManageAcl($mappings)
148			];
149		}
150
151		return $folderMap;
152	}
153
154	/**
155	 * @return array[]
156	 *
157	 * @psalm-return array<int, list<mixed>>
158	 * @throws Exception
159	 */
160	private function getAllFolderMappings(): array {
161		$query = $this->connection->getQueryBuilder();
162		$query->select('*')
163			->from('group_folders_manage');
164		$rows = $query->executeQuery()->fetchAll();
165
166		$folderMap = [];
167		foreach ($rows as $row) {
168			$id = (int)$row['folder_id'];
169
170			if (!isset($folderMap[$id])) {
171				$folderMap[$id] = [$row];
172			} else {
173				$folderMap[$id][] = $row;
174			}
175		}
176
177		return $folderMap;
178	}
179
180	private function getManageAcl(array $mappings): array {
181		return array_filter(array_map(function ($entry) {
182			if ($entry['mapping_type'] === 'user') {
183				$user = \OC::$server->get(IUserManager::class)->get($entry['mapping_id']);
184				if ($user === null) {
185					return null;
186				}
187				return [
188					'type' => 'user',
189					'id' => $user->getUID(),
190					'displayname' => $user->getDisplayName()
191				];
192			}
193			$group = \OC::$server->get(IGroupManager::class)->get($entry['mapping_id']);
194			if ($group === null) {
195				return [];
196			}
197			return [
198				'type' => 'group',
199				'id' => $group->getGID(),
200				'displayname' => $group->getDisplayName()
201			];
202		}, $mappings), function ($element) {
203			return $element !== null;
204		});
205	}
206
207	/**
208	 * @return (array|bool|int|mixed)[]|false
209	 *
210	 * @psalm-return array{id: mixed, mount_point: mixed, groups: array<empty, empty>|mixed, quota: int, size: int|mixed, acl: bool}|false
211	 * @throws Exception
212	 */
213	public function getFolder(int $id, int $rootStorageId) {
214		$applicableMap = $this->getAllApplicable();
215
216		$query = $this->connection->getQueryBuilder();
217
218		$query->select('folder_id', 'mount_point', 'quota', 'size', 'acl')
219			->from('group_folders', 'f')
220			->where($query->expr()->eq('folder_id', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
221		$this->joinQueryWithFileCache($query, $rootStorageId);
222
223		$result = $query->executeQuery();
224		$row = $result->fetch();
225		$result->closeCursor();
226
227		return $row ? [
228			'id' => $id,
229			'mount_point' => $row['mount_point'],
230			'groups' => $applicableMap[$id] ?? [],
231			'quota' => (int)$row['quota'],
232			'size' => $row['size'] ? $row['size'] : 0,
233			'acl' => (bool)$row['acl']
234		] : false;
235	}
236
237	public function getFolderByPath(string $path): int {
238		/** @var Node $node */
239		$node = \OC::$server->get(IRootFolder::class)->get($path);
240		/** @var GroupMountPoint $mountpoint */
241		$mountPoint = $node->getMountPoint();
242		return $mountPoint->getFolderId();
243	}
244
245	/**
246	 * @return int[][]
247	 *
248	 * @psalm-return array<int, array<array-key, int>>
249	 * @throws Exception
250	 */
251	private function getAllApplicable(): array {
252		$query = $this->connection->getQueryBuilder();
253
254		$query->select('folder_id', 'group_id', 'permissions')
255			->from('group_folders_groups');
256
257		$rows = $query->executeQuery()->fetchAll();
258
259		$applicableMap = [];
260		foreach ($rows as $row) {
261			$id = (int)$row['folder_id'];
262			if (!isset($applicableMap[$id])) {
263				$applicableMap[$id] = [];
264			}
265			$applicableMap[$id][$row['group_id']] = (int)$row['permissions'];
266		}
267
268		return $applicableMap;
269	}
270
271	/**
272	 * @throws Exception
273	 */
274	private function getGroups(int $id): array {
275		$groups = $this->getAllApplicable()[$id] ?? [];
276		$groups = array_map(function ($gid) {
277			return $this->groupManager->get($gid);
278		}, array_keys($groups));
279		return array_map(function ($group) {
280			return [
281				'gid' => $group->getGID(),
282				'displayname' => $group->getDisplayName()
283			];
284		}, array_filter($groups));
285	}
286
287	/**
288	 * Check if the user is able to configure the advanced folder permissions. This
289	 * is the case if the user is an admin, has admin permissions for the group folder
290	 * app or is member of a group that can manage permissions for the specific folder.
291	 * @throws Exception
292	 */
293	public function canManageACL(int $folderId, IUser $user): bool {
294		$userId = $user->getUId();
295		if ($this->groupManager->isAdmin($userId)) {
296			return true;
297		}
298
299		// Call private server api
300		if (class_exists('\OC\Settings\AuthorizedGroupMapper')) {
301			$authorizedGroupMapper = \OC::$server->get('\OC\Settings\AuthorizedGroupMapper');
302			$settingClasses = $authorizedGroupMapper->findAllClassesForUser($user);
303			if (in_array('OCA\GroupFolders\Settings\Admin', $settingClasses, true)) {
304				return true;
305			}
306		}
307
308		$query = $this->connection->getQueryBuilder();
309		$query->select('*')
310			->from('group_folders_manage')
311			->where($query->expr()->eq('folder_id', $query->createNamedParameter($folderId, IQueryBuilder::PARAM_INT)))
312			->andWhere($query->expr()->eq('mapping_type', $query->createNamedParameter('user')))
313			->andWhere($query->expr()->eq('mapping_id', $query->createNamedParameter($userId)));
314		if ($query->executeQuery()->rowCount() === 1) {
315			return true;
316		}
317
318		$query = $this->connection->getQueryBuilder();
319		$query->select('*')
320			->from('group_folders_manage')
321			->where($query->expr()->eq('folder_id', $query->createNamedParameter($folderId)))
322			->andWhere($query->expr()->eq('mapping_type', $query->createNamedParameter('group')));
323		$groups = $query->executeQuery()->fetchAll();
324		foreach ($groups as $manageRule) {
325			if ($this->groupManager->isInGroup($userId, $manageRule['mapping_id'])) {
326				return true;
327			}
328		}
329		return false;
330	}
331
332	/**
333	 * @throws Exception
334	 */
335	public function searchGroups(int $id, string $search = ''): array {
336		$groups = $this->getGroups($id);
337		if ($search === '') {
338			return $groups;
339		}
340		return array_filter($groups, function ($group) use ($search) {
341			return (stripos($group['gid'], $search) !== false) || (stripos($group['displayname'], $search) !== false);
342		});
343	}
344
345	/**
346	 * @throws Exception
347	 */
348	public function searchUsers(int $id, string $search = '', int $limit = 10, int $offset = 0): array {
349		$groups = $this->getGroups($id);
350		$users = [];
351		foreach ($groups as $groupArray) {
352			$group = $this->groupManager->get($groupArray['gid']);
353			if ($group) {
354				$foundUsers = $this->groupManager->displayNamesInGroup($group->getGID(), $search, $limit, $offset);
355				foreach ($foundUsers as $uid => $displayName) {
356					if (!isset($users[$uid])) {
357						$users[$uid] = [
358							'uid' => $uid,
359							'displayname' => $displayName
360						];
361					}
362				}
363			}
364		}
365		return array_values($users);
366	}
367
368	/**
369	 * @param string $groupId
370	 * @param int $rootStorageId
371	 * @return list<array{folder_id: int, mount_point: string, permissions: int, quota: int, acl: bool, rootCacheEntry: ?ICacheEntry}>
372	 * @throws Exception
373	 */
374	public function getFoldersForGroup(string $groupId, int $rootStorageId = 0): array {
375		$query = $this->connection->getQueryBuilder();
376
377		$query->select(
378			'f.folder_id',
379			'mount_point',
380			'quota',
381			'acl',
382			'fileid',
383			'storage',
384			'path',
385			'name',
386			'mimetype',
387			'mimepart',
388			'size',
389			'mtime',
390			'storage_mtime',
391			'etag',
392			'encrypted',
393			'parent'
394		)
395			->selectAlias('a.permissions', 'group_permissions')
396			->selectAlias('c.permissions', 'permissions')
397			->from('group_folders', 'f')
398			->innerJoin(
399				'f',
400				'group_folders_groups',
401				'a',
402				$query->expr()->eq('f.folder_id', 'a.folder_id')
403			)
404			->where($query->expr()->eq('a.group_id', $query->createNamedParameter($groupId)));
405		$this->joinQueryWithFileCache($query, $rootStorageId);
406
407		$result = $query->executeQuery()->fetchAll();
408		return array_map(function ($folder) {
409			return [
410				'folder_id' => (int)$folder['folder_id'],
411				'mount_point' => $folder['mount_point'],
412				'permissions' => (int)$folder['group_permissions'],
413				'quota' => (int)$folder['quota'],
414				'acl' => (bool)$folder['acl'],
415				'rootCacheEntry' => (isset($folder['fileid'])) ? Cache::cacheEntryFromData($folder, $this->mimeTypeLoader) : null
416			];
417		}, $result);
418	}
419
420	/**
421	 * @param string[] $groupIds
422	 * @param int $rootStorageId
423	 * @return list<array{folder_id: int, mount_point: string, permissions: int, quota: int, acl: bool, rootCacheEntry: ?ICacheEntry}>
424	 * @throws Exception
425	 */
426	public function getFoldersForGroups(array $groupIds, int $rootStorageId = 0): array {
427		$query = $this->connection->getQueryBuilder();
428
429		$query->select(
430			'f.folder_id',
431			'mount_point',
432			'quota',
433			'acl',
434			'fileid',
435			'storage',
436			'path',
437			'name',
438			'mimetype',
439			'mimepart',
440			'size',
441			'mtime',
442			'storage_mtime',
443			'etag',
444			'encrypted',
445			'parent'
446		)
447			->selectAlias('a.permissions', 'group_permissions')
448			->selectAlias('c.permissions', 'permissions')
449			->from('group_folders', 'f')
450			->innerJoin(
451				'f',
452				'group_folders_groups',
453				'a',
454				$query->expr()->eq('f.folder_id', 'a.folder_id')
455			)
456			->where($query->expr()->in('a.group_id', $query->createNamedParameter($groupIds, IQueryBuilder::PARAM_STR_ARRAY)));
457		$this->joinQueryWithFileCache($query, $rootStorageId);
458
459		$result = $query->executeQuery()->fetchAll();
460		return array_map(function ($folder) {
461			return [
462				'folder_id' => (int)$folder['folder_id'],
463				'mount_point' => $folder['mount_point'],
464				'permissions' => (int)$folder['group_permissions'],
465				'quota' => (int)$folder['quota'],
466				'acl' => (bool)$folder['acl'],
467				'rootCacheEntry' => (isset($folder['fileid'])) ? Cache::cacheEntryFromData($folder, $this->mimeTypeLoader) : null
468			];
469		}, $result);
470	}
471
472	/**
473	 * @throws Exception
474	 */
475	public function createFolder(string $mountPoint): int {
476		$query = $this->connection->getQueryBuilder();
477
478		$query->insert('group_folders')
479			->values([
480				'mount_point' => $query->createNamedParameter($mountPoint)
481			]);
482		$query->executeStatement();
483
484		return $query->getLastInsertId();
485	}
486
487	/**
488	 * @throws Exception
489	 */
490	public function setMountPoint(int $folderId, string $mountPoint): void {
491		$query = $this->connection->getQueryBuilder();
492
493		$query->update('group_folders')
494			->set('mount_point', $query->createNamedParameter($mountPoint))
495			->where($query->expr()->eq('folder_id', $query->createNamedParameter($folderId, IQueryBuilder::PARAM_INT)));
496		$query->executeStatement();
497	}
498
499	/**
500	 * @throws Exception
501	 */
502	public function addApplicableGroup(int $folderId, string $groupId): void {
503		$query = $this->connection->getQueryBuilder();
504
505		$query->insert('group_folders_groups')
506			->values([
507				'folder_id' => $query->createNamedParameter($folderId, IQueryBuilder::PARAM_INT),
508				'group_id' => $query->createNamedParameter($groupId),
509				'permissions' => $query->createNamedParameter(Constants::PERMISSION_ALL)
510			]);
511		$query->executeStatement();
512	}
513
514	/**
515	 * @throws Exception
516	 */
517	public function removeApplicableGroup(int $folderId, string $groupId): void {
518		$query = $this->connection->getQueryBuilder();
519
520		$query->delete('group_folders_groups')
521			->where($query->expr()->eq('folder_id', $query->createNamedParameter($folderId, IQueryBuilder::PARAM_INT)))
522			->andWhere($query->expr()->eq('group_id', $query->createNamedParameter($groupId)));
523		$query->executeStatement();
524	}
525
526	/**
527	 * @throws Exception
528	 */
529	public function setGroupPermissions(int $folderId, string $groupId, int $permissions): void {
530		$query = $this->connection->getQueryBuilder();
531
532		$query->update('group_folders_groups')
533			->set('permissions', $query->createNamedParameter($permissions, IQueryBuilder::PARAM_INT))
534			->where($query->expr()->eq('folder_id', $query->createNamedParameter($folderId, IQueryBuilder::PARAM_INT)))
535			->andWhere($query->expr()->eq('group_id', $query->createNamedParameter($groupId)));
536
537		$query->executeStatement();
538	}
539
540	/**
541	 * @throws Exception
542	 */
543	public function setManageACL(int $folderId, string $type, string $id, bool $manageAcl): void {
544		$query = $this->connection->getQueryBuilder();
545		if ($manageAcl === true) {
546			$query->insert('group_folders_manage')
547				->values([
548					'folder_id' => $query->createNamedParameter($folderId, IQueryBuilder::PARAM_INT),
549					'mapping_type' => $query->createNamedParameter($type),
550					'mapping_id' => $query->createNamedParameter($id)
551				]);
552		} else {
553			$query->delete('group_folders_manage')
554				->where($query->expr()->eq('folder_id', $query->createNamedParameter($folderId, IQueryBuilder::PARAM_INT)))
555				->andWhere($query->expr()->eq('mapping_type', $query->createNamedParameter($type)))
556				->andWhere($query->expr()->eq('mapping_id', $query->createNamedParameter($id)));
557		}
558		$query->executeStatement();
559	}
560
561	/**
562	 * @throws Exception
563	 */
564	public function removeFolder(int $folderId): void {
565		$query = $this->connection->getQueryBuilder();
566
567		$query->delete('group_folders')
568			->where($query->expr()->eq('folder_id', $query->createNamedParameter($folderId, IQueryBuilder::PARAM_INT)));
569		$query->executeStatement();
570	}
571
572	/**
573	 * @throws Exception
574	 */
575	public function setFolderQuota(int $folderId, int $quota): void {
576		$query = $this->connection->getQueryBuilder();
577
578		$query->update('group_folders')
579			->set('quota', $query->createNamedParameter($quota))
580			->where($query->expr()->eq('folder_id', $query->createNamedParameter($folderId)));
581		$query->executeStatement();
582	}
583
584	/**
585	 * @throws Exception
586	 */
587	public function renameFolder(int $folderId, string $newMountPoint): void {
588		$query = $this->connection->getQueryBuilder();
589
590		$query->update('group_folders')
591			->set('mount_point', $query->createNamedParameter($newMountPoint))
592			->where($query->expr()->eq('folder_id', $query->createNamedParameter($folderId, IQueryBuilder::PARAM_INT)));
593		$query->executeStatement();
594	}
595
596	/**
597	 * @throws Exception
598	 */
599	public function deleteGroup(string $groupId): void {
600		$query = $this->connection->getQueryBuilder();
601
602		$query->delete('group_folders_groups')
603			->where($query->expr()->eq('group_id', $query->createNamedParameter($groupId)));
604		$query->executeStatement();
605	}
606
607	/**
608	 * @throws Exception
609	 */
610	public function setFolderACL(int $folderId, bool $acl): void {
611		$query = $this->connection->getQueryBuilder();
612
613		$query->update('group_folders')
614			->set('acl', $query->createNamedParameter((int)$acl, IQueryBuilder::PARAM_INT))
615			->where($query->expr()->eq('folder_id', $query->createNamedParameter($folderId)));
616		$query->executeStatement();
617
618		if ($acl === false) {
619			$query = $this->connection->getQueryBuilder();
620			$query->delete('group_folders_manage')
621				->where($query->expr()->eq('folder_id', $query->createNamedParameter($folderId)));
622			$query->executeStatement();
623		}
624	}
625
626	/**
627	 * @param IUser $user
628	 * @param int $rootStorageId
629	 * @return array[]
630	 * @throws Exception
631	 */
632	public function getFoldersForUser(IUser $user, int $rootStorageId = 0): array {
633		$groups = $this->groupManager->getUserGroupIds($user);
634		$folders = $this->getFoldersForGroups($groups, $rootStorageId);
635
636		$mergedFolders = [];
637		foreach ($folders as $folder) {
638			$id = (int)$folder['folder_id'];
639			if (isset($mergedFolders[$id])) {
640				$mergedFolders[$id]['permissions'] |= $folder['permissions'];
641			} else {
642				$mergedFolders[$id] = $folder;
643			}
644		}
645
646		return array_values($mergedFolders);
647	}
648
649	/**
650	 * @param IUser $user
651	 * @param int $folderId
652	 * @return int
653	 * @throws Exception
654	 */
655	public function getFolderPermissionsForUser(IUser $user, int $folderId): int {
656		$groups = $this->groupManager->getUserGroupIds($user);
657		$folders = $this->getFoldersForGroups($groups);
658
659		$permissions = 0;
660		foreach ($folders as $folder) {
661			if ($folderId === (int)$folder['folder_id']) {
662				$permissions |= $folder['permissions'];
663			}
664		}
665
666		return $permissions;
667	}
668}
669