Passed
Push — master ( 68a02b...b29c3a )
by Christoph
15:41 queued 11s
created

isRemoteGroupSharingAllowed()   A

Complexity

Conditions 2
Paths 3

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 3
nop 1
dl 0
loc 7
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * @copyright Copyright (c) 2016, ownCloud, Inc.
7
 *
8
 * @author Arthur Schiwon <[email protected]>
9
 * @author Bjoern Schiessle <[email protected]>
10
 * @author Björn Schießle <[email protected]>
11
 * @author Christoph Wurst <[email protected]>
12
 * @author Daniel Calviño Sánchez <[email protected]>
13
 * @author Daniel Kesselberg <[email protected]>
14
 * @author Joas Schilling <[email protected]>
15
 * @author John Molakvoæ (skjnldsv) <[email protected]>
16
 * @author Julius Härtl <[email protected]>
17
 * @author Maxence Lange <[email protected]>
18
 * @author Morris Jobke <[email protected]>
19
 * @author Robin Appelman <[email protected]>
20
 * @author Roeland Jago Douma <[email protected]>
21
 *
22
 * @license AGPL-3.0
23
 *
24
 * This code is free software: you can redistribute it and/or modify
25
 * it under the terms of the GNU Affero General Public License, version 3,
26
 * as published by the Free Software Foundation.
27
 *
28
 * This program is distributed in the hope that it will be useful,
29
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
30
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
31
 * GNU Affero General Public License for more details.
32
 *
33
 * You should have received a copy of the GNU Affero General Public License, version 3,
34
 * along with this program. If not, see <http://www.gnu.org/licenses/>
35
 *
36
 */
37
38
namespace OCA\Files_Sharing\Controller;
39
40
use OCP\Constants;
41
use function array_slice;
42
use function array_values;
43
use Generator;
44
use OC\Collaboration\Collaborators\SearchResult;
45
use OCP\AppFramework\Http\DataResponse;
46
use OCP\AppFramework\OCS\OCSBadRequestException;
47
use OCP\AppFramework\OCSController;
48
use OCP\Collaboration\Collaborators\ISearch;
49
use OCP\Collaboration\Collaborators\ISearchResult;
50
use OCP\Collaboration\Collaborators\SearchResultType;
51
use OCP\IConfig;
52
use OCP\IRequest;
53
use OCP\IURLGenerator;
54
use OCP\Share\IShare;
55
use OCP\Share\IManager;
56
use function usort;
57
58
class 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