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\Mount;
23
24use OC\Files\Storage\Wrapper\Jail;
25use OC\Files\Storage\Wrapper\PermissionsMask;
26use OCA\GroupFolders\ACL\ACLManagerFactory;
27use OCA\GroupFolders\ACL\ACLStorageWrapper;
28use OCA\GroupFolders\Folder\FolderManager;
29use OCP\DB\QueryBuilder\IQueryBuilder;
30use OCP\Files\Config\IMountProvider;
31use OCP\Files\Config\IMountProviderCollection;
32use OCP\Files\Folder;
33use OCP\Files\Node;
34use OCP\Files\Cache\ICacheEntry;
35use OCP\Files\Mount\IMountPoint;
36use OCP\Files\NotFoundException;
37use OCP\Files\Storage\IStorage;
38use OCP\Files\Storage\IStorageFactory;
39use OCP\IDBConnection;
40use OCP\IGroupManager;
41use OCP\IRequest;
42use OCP\ISession;
43use OCP\IUser;
44use OCP\IUserSession;
45
46class MountProvider implements IMountProvider {
47	/** @var IGroupManager */
48	private $groupProvider;
49
50	/** @var callable */
51	private $rootProvider;
52
53	/** @var Folder|null */
54	private $root = null;
55
56	/** @var FolderManager */
57	private $folderManager;
58
59	private $aclManagerFactory;
60
61	private $userSession;
62
63	private $request;
64
65	private $session;
66
67	private $mountProviderCollection;
68	private $connection;
69
70	public function __construct(
71		IGroupManager $groupProvider,
72		FolderManager $folderManager,
73		callable $rootProvider,
74		ACLManagerFactory $aclManagerFactory,
75		IUserSession $userSession,
76		IRequest $request,
77		ISession $session,
78		IMountProviderCollection $mountProviderCollection,
79		IDBConnection $connection
80	) {
81		$this->groupProvider = $groupProvider;
82		$this->folderManager = $folderManager;
83		$this->rootProvider = $rootProvider;
84		$this->aclManagerFactory = $aclManagerFactory;
85		$this->userSession = $userSession;
86		$this->request = $request;
87		$this->session = $session;
88		$this->mountProviderCollection = $mountProviderCollection;
89		$this->connection = $connection;
90	}
91
92	/**
93	 * @return list<array{folder_id: int, mount_point: string, permissions: int, quota: int, acl: bool, rootCacheEntry: ?ICacheEntry}>
94	 */
95	public function getFoldersForUser(IUser $user): array {
96		return $this->folderManager->getFoldersForUser($user, $this->getRootFolder()->getStorage()->getCache()->getNumericStorageId());
97	}
98
99	public function getMountsForUser(IUser $user, IStorageFactory $loader) {
100		$folders = $this->getFoldersForUser($user);
101
102		$mountPoints = array_map(function (array $folder) {
103			return 'files/' . $folder['mount_point'];
104		}, $folders);
105		$conflicts = $this->findConflictsForUser($user, $mountPoints);
106
107		return array_values(array_filter(array_map(function ($folder) use ($user, $loader, $conflicts) {
108			// check for existing files in the user home and rename them if needed
109			$originalFolderName = $folder['mount_point'];
110			if (in_array($originalFolderName, $conflicts)) {
111				/** @var IStorage $userStorage */
112				$userStorage = $this->mountProviderCollection->getHomeMountForUser($user)->getStorage();
113				$userCache = $userStorage->getCache();
114				$i = 1;
115				$folderName = $folder['mount_point'] . ' (' . $i++ . ')';
116
117				while ($userCache->inCache("files/$folderName")) {
118					$folderName = $originalFolderName . ' (' . $i++ . ')';
119				}
120
121				$userStorage->rename("files/$originalFolderName", "files/$folderName");
122				$userCache->move("files/$originalFolderName", "files/$folderName");
123				$userStorage->getPropagator()->propagateChange("files/$folderName", time());
124			}
125
126			return $this->getMount(
127				$folder['folder_id'],
128				'/' . $user->getUID() . '/files/' . $folder['mount_point'],
129				$folder['permissions'],
130				$folder['quota'],
131				$folder['rootCacheEntry'],
132				$loader,
133				$folder['acl'],
134				$user
135			);
136		}, $folders)));
137	}
138
139	private function getCurrentUID(): ?string {
140		try {
141			// wopi requests are not logged in, instead we need to get the editor user from the access token
142			if (strpos($this->request->getRawPathInfo(), 'apps/richdocuments/wopi') && class_exists('OCA\Richdocuments\Db\WopiMapper')) {
143				$wopiMapper = \OC::$server->query('OCA\Richdocuments\Db\WopiMapper');
144				$token = $this->request->getParam('access_token');
145				if ($token) {
146					$wopi = $wopiMapper->getPathForToken($token);
147					return $wopi->getEditorUid();
148				}
149			}
150		} catch (\Exception $e) {
151		}
152
153		$user = $this->userSession->getUser();
154		return $user ? $user->getUID() : null;
155	}
156
157	public function getMount(int $id, string $mountPoint, int $permissions, int $quota, ?ICacheEntry $cacheEntry = null, IStorageFactory $loader = null, bool $acl = false, IUser $user = null): ?IMountPoint {
158		if (!$cacheEntry) {
159			// trigger folder creation
160			$folder = $this->getFolder($id);
161			if ($folder === null) {
162				return null;
163			}
164			$cacheEntry = $this->getRootFolder()->getStorage()->getCache()->get($folder->getId());
165		}
166
167		$storage = $this->getRootFolder()->getStorage();
168
169		$rootPath = $this->getJailPath($id);
170
171		// apply acl before jail
172		if ($acl && $user) {
173			$inShare = $this->getCurrentUID() === null || $this->getCurrentUID() !== $user->getUID();
174			$aclManager = $this->aclManagerFactory->getACLManager($user);
175			$storage = new ACLStorageWrapper([
176				'storage' => $storage,
177				'acl_manager' => $aclManager,
178				'in_share' => $inShare
179			]);
180			$aclRootPermissions = $aclManager->getACLPermissionsForPath($rootPath);
181			$cacheEntry['permissions'] &= $aclRootPermissions;
182		}
183
184		$baseStorage = new Jail([
185			'storage' => $storage,
186			'root' => $rootPath
187		]);
188		$quotaStorage = new GroupFolderStorage([
189			'storage' => $baseStorage,
190			'quota' => $quota,
191			'folder_id' => $id,
192			'rootCacheEntry' => $cacheEntry,
193			'userSession' => $this->userSession,
194			'mountOwner' => $user,
195		]);
196		$maskedStore = new PermissionsMask([
197			'storage' => $quotaStorage,
198			'mask' => $permissions
199		]);
200
201		return new GroupMountPoint(
202			$id,
203			$maskedStore,
204			$mountPoint,
205			null,
206			$loader
207		);
208	}
209
210	public function getJailPath(int $folderId): string {
211		return $this->getRootFolder()->getInternalPath() . '/' . $folderId;
212	}
213
214	private function getRootFolder(): Folder {
215		if (is_null($this->root)) {
216			$rootProvider = $this->rootProvider;
217			$this->root = $rootProvider();
218		}
219		return $this->root;
220	}
221
222	public function getFolder(int $id, bool $create = true): ?Node {
223		try {
224			return $this->getRootFolder()->get((string)$id);
225		} catch (NotFoundException $e) {
226			if ($create) {
227				return $this->getRootFolder()->newFolder((string)$id);
228			} else {
229				return null;
230			}
231		}
232	}
233
234	/**
235	 * @param string[] $mountPoints
236	 * @return string[] An array of paths.
237	 */
238	private function findConflictsForUser(IUser $user, array $mountPoints): array {
239		$userHome = $this->mountProviderCollection->getHomeMountForUser($user);
240
241		$pathHashes = array_map('md5', $mountPoints);
242
243		$query = $this->connection->getQueryBuilder();
244		$query->select('path')
245			->from('filecache')
246			->where($query->expr()->eq('storage', $query->createNamedParameter($userHome->getNumericStorageId(), IQueryBuilder::PARAM_INT)))
247			->andWhere($query->expr()->in('path_hash', $query->createNamedParameter($pathHashes, IQueryBuilder::PARAM_STR_ARRAY)));
248
249		$paths = $query->execute()->fetchAll(\PDO::FETCH_COLUMN);
250		return array_map(function ($path) {
251			return substr($path, 6); // strip leading "files/"
252		}, $paths);
253	}
254}
255