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