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