1<?php
2
3declare(strict_types=1);
4
5/**
6 * @copyright Copyright (c) 2016, ownCloud, Inc.
7 *
8 * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
9 * @author Bjoern Schiessle <bjoern@schiessle.org>
10 * @author Björn Schießle <bjoern@schiessle.org>
11 * @author Christoph Wurst <christoph@winzerhof-wurst.at>
12 * @author Daniel Calviño Sánchez <danxuliu@gmail.com>
13 * @author Daniel Kesselberg <mail@danielkesselberg.de>
14 * @author J0WI <J0WI@users.noreply.github.com>
15 * @author Joas Schilling <coding@schilljs.com>
16 * @author John Molakvoæ <skjnldsv@protonmail.com>
17 * @author Julius Härtl <jus@bitgrid.net>
18 * @author Maxence Lange <maxence@nextcloud.com>
19 * @author Morris Jobke <hey@morrisjobke.de>
20 * @author Robin Appelman <robin@icewind.nl>
21 * @author Roeland Jago Douma <roeland@famdouma.nl>
22 *
23 * @license AGPL-3.0
24 *
25 * This code is free software: you can redistribute it and/or modify
26 * it under the terms of the GNU Affero General Public License, version 3,
27 * as published by the Free Software Foundation.
28 *
29 * This program is distributed in the hope that it will be useful,
30 * but WITHOUT ANY WARRANTY; without even the implied warranty of
31 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
32 * GNU Affero General Public License for more details.
33 *
34 * You should have received a copy of the GNU Affero General Public License, version 3,
35 * along with this program. If not, see <http://www.gnu.org/licenses/>
36 *
37 */
38namespace OCA\Files_Sharing\Controller;
39
40use OCP\Constants;
41use function array_slice;
42use function array_values;
43use Generator;
44use OC\Collaboration\Collaborators\SearchResult;
45use OCP\AppFramework\Http\DataResponse;
46use OCP\AppFramework\OCS\OCSBadRequestException;
47use OCP\AppFramework\OCSController;
48use OCP\Collaboration\Collaborators\ISearch;
49use OCP\Collaboration\Collaborators\ISearchResult;
50use OCP\Collaboration\Collaborators\SearchResultType;
51use OCP\IConfig;
52use OCP\IRequest;
53use OCP\IURLGenerator;
54use OCP\Share\IShare;
55use OCP\Share\IManager;
56use function usort;
57
58class ShareesAPIController extends OCSController {
59
60	/** @var string */
61	protected $userId;
62
63	/** @var IConfig */
64	protected $config;
65
66	/** @var IURLGenerator */
67	protected $urlGenerator;
68
69	/** @var IManager */
70	protected $shareManager;
71
72	/** @var int */
73	protected $offset = 0;
74
75	/** @var int */
76	protected $limit = 10;
77
78	/** @var array */
79	protected $result = [
80		'exact' => [
81			'users' => [],
82			'groups' => [],
83			'remotes' => [],
84			'remote_groups' => [],
85			'emails' => [],
86			'circles' => [],
87			'rooms' => [],
88			'deck' => [],
89		],
90		'users' => [],
91		'groups' => [],
92		'remotes' => [],
93		'remote_groups' => [],
94		'emails' => [],
95		'lookup' => [],
96		'circles' => [],
97		'rooms' => [],
98		'deck' => [],
99		'lookupEnabled' => false,
100	];
101
102	protected $reachedEndFor = [];
103	/** @var ISearch */
104	private $collaboratorSearch;
105
106	/**
107	 * @param string $UserId
108	 * @param string $appName
109	 * @param IRequest $request
110	 * @param IConfig $config
111	 * @param IURLGenerator $urlGenerator
112	 * @param IManager $shareManager
113	 * @param ISearch $collaboratorSearch
114	 */
115	public function __construct(
116		$UserId,
117		string $appName,
118		IRequest $request,
119		IConfig $config,
120		IURLGenerator $urlGenerator,
121		IManager $shareManager,
122		ISearch $collaboratorSearch
123	) {
124		parent::__construct($appName, $request);
125		$this->userId = $UserId;
126		$this->config = $config;
127		$this->urlGenerator = $urlGenerator;
128		$this->shareManager = $shareManager;
129		$this->collaboratorSearch = $collaboratorSearch;
130	}
131
132	/**
133	 * @NoAdminRequired
134	 *
135	 * @param string $search
136	 * @param string $itemType
137	 * @param int $page
138	 * @param int $perPage
139	 * @param int|int[] $shareType
140	 * @param bool $lookup
141	 * @return DataResponse
142	 * @throws OCSBadRequestException
143	 */
144	public function search(string $search = '', string $itemType = null, int $page = 1, int $perPage = 200, $shareType = null, bool $lookup = false): DataResponse {
145
146		// only search for string larger than a given threshold
147		$threshold = $this->config->getSystemValueInt('sharing.minSearchStringLength', 0);
148		if (strlen($search) < $threshold) {
149			return new DataResponse($this->result);
150		}
151
152		// never return more than the max. number of results configured in the config.php
153		$maxResults = $this->config->getSystemValueInt('sharing.maxAutocompleteResults', Constants::SHARING_MAX_AUTOCOMPLETE_RESULTS_DEFAULT);
154		if ($maxResults > 0) {
155			$perPage = min($perPage, $maxResults);
156		}
157		if ($perPage <= 0) {
158			throw new OCSBadRequestException('Invalid perPage argument');
159		}
160		if ($page <= 0) {
161			throw new OCSBadRequestException('Invalid page');
162		}
163
164		$shareTypes = [
165			IShare::TYPE_USER,
166		];
167
168		if ($itemType === null) {
169			throw new OCSBadRequestException('Missing itemType');
170		} elseif ($itemType === 'file' || $itemType === 'folder') {
171			if ($this->shareManager->allowGroupSharing()) {
172				$shareTypes[] = IShare::TYPE_GROUP;
173			}
174
175			if ($this->isRemoteSharingAllowed($itemType)) {
176				$shareTypes[] = IShare::TYPE_REMOTE;
177			}
178
179			if ($this->isRemoteGroupSharingAllowed($itemType)) {
180				$shareTypes[] = IShare::TYPE_REMOTE_GROUP;
181			}
182
183			if ($this->shareManager->shareProviderExists(IShare::TYPE_EMAIL)) {
184				$shareTypes[] = IShare::TYPE_EMAIL;
185			}
186
187			if ($this->shareManager->shareProviderExists(IShare::TYPE_ROOM)) {
188				$shareTypes[] = IShare::TYPE_ROOM;
189			}
190
191			if ($this->shareManager->shareProviderExists(IShare::TYPE_DECK)) {
192				$shareTypes[] = IShare::TYPE_DECK;
193			}
194		} else {
195			if ($this->shareManager->allowGroupSharing()) {
196				$shareTypes[] = IShare::TYPE_GROUP;
197			}
198			$shareTypes[] = IShare::TYPE_EMAIL;
199		}
200
201		// FIXME: DI
202		if (\OC::$server->getAppManager()->isEnabledForUser('circles') && class_exists('\OCA\Circles\ShareByCircleProvider')) {
203			$shareTypes[] = IShare::TYPE_CIRCLE;
204		}
205
206		if ($this->shareManager->shareProviderExists(IShare::TYPE_DECK)) {
207			$shareTypes[] = IShare::TYPE_DECK;
208		}
209
210		if ($shareType !== null && is_array($shareType)) {
211			$shareTypes = array_intersect($shareTypes, $shareType);
212		} elseif (is_numeric($shareType)) {
213			$shareTypes = array_intersect($shareTypes, [(int) $shareType]);
214		}
215		sort($shareTypes);
216
217		$this->limit = $perPage;
218		$this->offset = $perPage * ($page - 1);
219
220		// In global scale mode we always search the loogup server
221		if ($this->config->getSystemValueBool('gs.enabled', false)) {
222			$lookup = true;
223			$this->result['lookupEnabled'] = true;
224		} else {
225			$this->result['lookupEnabled'] = $this->config->getAppValue('files_sharing', 'lookupServerEnabled', 'yes') === 'yes';
226		}
227
228		[$result, $hasMoreResults] = $this->collaboratorSearch->search($search, $shareTypes, $lookup, $this->limit, $this->offset);
229
230		// extra treatment for 'exact' subarray, with a single merge expected keys might be lost
231		if (isset($result['exact'])) {
232			$result['exact'] = array_merge($this->result['exact'], $result['exact']);
233		}
234		$this->result = array_merge($this->result, $result);
235		$response = new DataResponse($this->result);
236
237		if ($hasMoreResults) {
238			$response->addHeader('Link', $this->getPaginationLink($page, [
239				'search' => $search,
240				'itemType' => $itemType,
241				'shareType' => $shareTypes,
242				'perPage' => $perPage,
243			]));
244		}
245
246		return $response;
247	}
248
249	/**
250	 * @param string $user
251	 * @param int $shareType
252	 *
253	 * @return Generator<array<string>>
254	 */
255	private function getAllShareesByType(string $user, int $shareType): Generator {
256		$offset = 0;
257		$pageSize = 50;
258
259		while (count($page = $this->shareManager->getSharesBy(
260			$user,
261			$shareType,
262			null,
263			false,
264			$pageSize,
265			$offset
266		))) {
267			foreach ($page as $share) {
268				yield [$share->getSharedWith(), $share->getSharedWithDisplayName() ?? $share->getSharedWith()];
269			}
270
271			$offset += $pageSize;
272		}
273	}
274
275	private function sortShareesByFrequency(array $sharees): array {
276		usort($sharees, function (array $s1, array $s2) {
277			return $s2['count'] - $s1['count'];
278		});
279		return $sharees;
280	}
281
282	private $searchResultTypeMap = [
283		IShare::TYPE_USER => 'users',
284		IShare::TYPE_GROUP => 'groups',
285		IShare::TYPE_REMOTE => 'remotes',
286		IShare::TYPE_REMOTE_GROUP => 'remote_groups',
287		IShare::TYPE_EMAIL => 'emails',
288	];
289
290	private function getAllSharees(string $user, array $shareTypes): ISearchResult {
291		$result = [];
292		foreach ($shareTypes as $shareType) {
293			$sharees = $this->getAllShareesByType($user, $shareType);
294			$shareTypeResults = [];
295			foreach ($sharees as [$sharee, $displayname]) {
296				if (!isset($this->searchResultTypeMap[$shareType])) {
297					continue;
298				}
299
300				if (!isset($shareTypeResults[$sharee])) {
301					$shareTypeResults[$sharee] = [
302						'count' => 1,
303						'label' => $displayname,
304						'value' => [
305							'shareType' => $shareType,
306							'shareWith' => $sharee,
307						],
308					];
309				} else {
310					$shareTypeResults[$sharee]['count']++;
311				}
312			}
313			$result = array_merge($result, array_values($shareTypeResults));
314		}
315
316		$top5 = array_slice(
317			$this->sortShareesByFrequency($result),
318			0,
319			5
320		);
321
322		$searchResult = new SearchResult();
323		foreach ($this->searchResultTypeMap as $int => $str) {
324			$searchResult->addResultSet(new SearchResultType($str), [], []);
325			foreach ($top5 as $x) {
326				if ($x['value']['shareType'] === $int) {
327					$searchResult->addResultSet(new SearchResultType($str), [], [$x]);
328				}
329			}
330		}
331		return $searchResult;
332	}
333
334	/**
335	 * @NoAdminRequired
336	 *
337	 * @param string $itemType
338	 * @return DataResponse
339	 * @throws OCSBadRequestException
340	 */
341	public function findRecommended(string $itemType = null, $shareType = null): DataResponse {
342		$shareTypes = [
343			IShare::TYPE_USER,
344		];
345
346		if ($itemType === null) {
347			throw new OCSBadRequestException('Missing itemType');
348		} elseif ($itemType === 'file' || $itemType === 'folder') {
349			if ($this->shareManager->allowGroupSharing()) {
350				$shareTypes[] = IShare::TYPE_GROUP;
351			}
352
353			if ($this->isRemoteSharingAllowed($itemType)) {
354				$shareTypes[] = IShare::TYPE_REMOTE;
355			}
356
357			if ($this->isRemoteGroupSharingAllowed($itemType)) {
358				$shareTypes[] = IShare::TYPE_REMOTE_GROUP;
359			}
360
361			if ($this->shareManager->shareProviderExists(IShare::TYPE_EMAIL)) {
362				$shareTypes[] = IShare::TYPE_EMAIL;
363			}
364
365			if ($this->shareManager->shareProviderExists(IShare::TYPE_ROOM)) {
366				$shareTypes[] = IShare::TYPE_ROOM;
367			}
368		} else {
369			$shareTypes[] = IShare::TYPE_GROUP;
370			$shareTypes[] = IShare::TYPE_EMAIL;
371		}
372
373		// FIXME: DI
374		if (\OC::$server->getAppManager()->isEnabledForUser('circles') && class_exists('\OCA\Circles\ShareByCircleProvider')) {
375			$shareTypes[] = IShare::TYPE_CIRCLE;
376		}
377
378		if (isset($_GET['shareType']) && is_array($_GET['shareType'])) {
379			$shareTypes = array_intersect($shareTypes, $_GET['shareType']);
380			sort($shareTypes);
381		} elseif (is_numeric($shareType)) {
382			$shareTypes = array_intersect($shareTypes, [(int) $shareType]);
383			sort($shareTypes);
384		}
385
386		return new DataResponse(
387			$this->getAllSharees($this->userId, $shareTypes)->asArray()
388		);
389	}
390
391	/**
392	 * Method to get out the static call for better testing
393	 *
394	 * @param string $itemType
395	 * @return bool
396	 */
397	protected function isRemoteSharingAllowed(string $itemType): bool {
398		try {
399			// FIXME: static foo makes unit testing unnecessarily difficult
400			$backend = \OC\Share\Share::getBackend($itemType);
401			return $backend->isShareTypeAllowed(IShare::TYPE_REMOTE);
402		} catch (\Exception $e) {
403			return false;
404		}
405	}
406
407	protected function isRemoteGroupSharingAllowed(string $itemType): bool {
408		try {
409			// FIXME: static foo makes unit testing unnecessarily difficult
410			$backend = \OC\Share\Share::getBackend($itemType);
411			return $backend->isShareTypeAllowed(IShare::TYPE_REMOTE_GROUP);
412		} catch (\Exception $e) {
413			return false;
414		}
415	}
416
417
418	/**
419	 * Generates a bunch of pagination links for the current page
420	 *
421	 * @param int $page Current page
422	 * @param array $params Parameters for the URL
423	 * @return string
424	 */
425	protected function getPaginationLink(int $page, array $params): string {
426		if ($this->isV2()) {
427			$url = $this->urlGenerator->getAbsoluteURL('/ocs/v2.php/apps/files_sharing/api/v1/sharees') . '?';
428		} else {
429			$url = $this->urlGenerator->getAbsoluteURL('/ocs/v1.php/apps/files_sharing/api/v1/sharees') . '?';
430		}
431		$params['page'] = $page + 1;
432		return '<' . $url . http_build_query($params) . '>; rel="next"';
433	}
434
435	/**
436	 * @return bool
437	 */
438	protected function isV2(): bool {
439		return $this->request->getScriptName() === '/ocs/v2.php';
440	}
441}
442