1<?php 2 3declare(strict_types=1); 4/** 5 * 6 * @copyright Copyright (c) 2018, Daniel Calviño Sánchez (danxuliu@gmail.com) 7 * 8 * Code copied from "lib/private/Share20/DefaultShareProvider.php" and 9 * "apps/sharebymail/lib/ShareByMailProvider.php" at d805959e819e64 in Nextcloud 10 * server repository. 11 * 12 * @license GNU AGPL version 3 or any later version 13 * 14 * This program is free software: you can redistribute it and/or modify 15 * it under the terms of the GNU Affero General Public License as 16 * published by the Free Software Foundation, either version 3 of the 17 * License, or (at your option) any later version. 18 * 19 * This program is distributed in the hope that it will be useful, 20 * but WITHOUT ANY WARRANTY; without even the implied warranty of 21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 22 * GNU Affero General Public License for more details. 23 * 24 * You should have received a copy of the GNU Affero General Public License 25 * along with this program. If not, see <http://www.gnu.org/licenses/>. 26 * 27 */ 28 29namespace OCA\Talk\Share; 30 31use OC\Files\Cache\Cache; 32use OCA\Talk\Events\ParticipantEvent; 33use OCA\Talk\Events\RemoveUserEvent; 34use OCA\Talk\Events\RoomEvent; 35use OCA\Talk\Exceptions\ParticipantNotFoundException; 36use OCA\Talk\Exceptions\RoomNotFoundException; 37use OCA\Talk\Manager; 38use OCA\Talk\Participant; 39use OCA\Talk\Room; 40use OCA\Talk\Service\ParticipantService; 41use OCP\AppFramework\Utility\ITimeFactory; 42use OCP\DB\QueryBuilder\IQueryBuilder; 43use OCP\EventDispatcher\IEventDispatcher; 44use OCP\Files\Folder; 45use OCP\Files\IMimeTypeLoader; 46use OCP\Files\Node; 47use OCP\IDBConnection; 48use OCP\IL10N; 49use OCP\Security\ISecureRandom; 50use OCP\Share\Exceptions\GenericShareException; 51use OCP\Share\Exceptions\ShareNotFound; 52use OCP\Share\IManager as IShareManager; 53use OCP\Share\IShare; 54use OCP\Share\IShareProvider; 55use Symfony\Component\EventDispatcher\EventDispatcherInterface; 56use Symfony\Component\EventDispatcher\GenericEvent; 57 58/** 59 * Share provider for room shares. 60 * 61 * Files are shared with a room identified by its token; only users currently in 62 * the room can share with and access the shared files (although the access 63 * checks are not enforced by the provider, but done on a higher layer). 64 * 65 * Like in group shares, a recipient can move or delete a share without 66 * modifying the share for the other users in the room. 67 */ 68class RoomShareProvider implements IShareProvider { 69 70 // Special share type for user modified room shares 71 public const SHARE_TYPE_USERROOM = 11; 72 73 public const TALK_FOLDER = '/Talk'; 74 public const TALK_FOLDER_PLACEHOLDER = '/{TALK_PLACEHOLDER}'; 75 76 /** @var IDBConnection */ 77 private $dbConnection; 78 /** @var ISecureRandom */ 79 private $secureRandom; 80 /** @var IShareManager */ 81 private $shareManager; 82 /** @var EventDispatcherInterface */ 83 private $dispatcher; 84 /** @var Manager */ 85 private $manager; 86 /** @var ParticipantService */ 87 private $participantService; 88 /** @var ITimeFactory */ 89 protected $timeFactory; 90 /** @var IL10N */ 91 private $l; 92 /** @var IMimeTypeLoader */ 93 private $mimeTypeLoader; 94 95 public function __construct( 96 IDBConnection $connection, 97 ISecureRandom $secureRandom, 98 IShareManager $shareManager, 99 EventDispatcherInterface $dispatcher, 100 Manager $manager, 101 ParticipantService $participantService, 102 ITimeFactory $timeFactory, 103 IL10N $l, 104 IMimeTypeLoader $mimeTypeLoader 105 ) { 106 $this->dbConnection = $connection; 107 $this->secureRandom = $secureRandom; 108 $this->shareManager = $shareManager; 109 $this->dispatcher = $dispatcher; 110 $this->manager = $manager; 111 $this->participantService = $participantService; 112 $this->timeFactory = $timeFactory; 113 $this->l = $l; 114 $this->mimeTypeLoader = $mimeTypeLoader; 115 } 116 117 public static function register(IEventDispatcher $dispatcher): void { 118 $listener = static function (ParticipantEvent $event): void { 119 $room = $event->getRoom(); 120 121 if ($event->getParticipant()->getAttendee()->getParticipantType() === Participant::USER_SELF_JOINED) { 122 /** @var self $roomShareProvider */ 123 $roomShareProvider = \OC::$server->query(self::class); 124 $roomShareProvider->deleteInRoom($room->getToken(), $event->getParticipant()->getAttendee()->getActorId()); 125 } 126 }; 127 $dispatcher->addListener(Room::EVENT_AFTER_ROOM_DISCONNECT, $listener); 128 129 $listener = static function (RemoveUserEvent $event): void { 130 $room = $event->getRoom(); 131 132 /** @var self $roomShareProvider */ 133 $roomShareProvider = \OC::$server->query(self::class); 134 $roomShareProvider->deleteInRoom($room->getToken(), $event->getUser()->getUID()); 135 }; 136 $dispatcher->addListener(Room::EVENT_AFTER_USER_REMOVE, $listener); 137 138 $listener = static function (RoomEvent $event): void { 139 $room = $event->getRoom(); 140 141 /** @var self $roomShareProvider */ 142 $roomShareProvider = \OC::$server->query(self::class); 143 $roomShareProvider->deleteInRoom($room->getToken()); 144 }; 145 $dispatcher->addListener(Room::EVENT_AFTER_ROOM_DELETE, $listener); 146 } 147 148 /** 149 * Return the identifier of this provider. 150 * 151 * @return string Containing only [a-zA-Z0-9] 152 */ 153 public function identifier(): string { 154 return 'ocRoomShare'; 155 } 156 157 /** 158 * Create a share 159 * 160 * @param IShare $share 161 * @return IShare The share object 162 * @throws GenericShareException 163 */ 164 public function create(IShare $share): IShare { 165 try { 166 $room = $this->manager->getRoomByToken($share->getSharedWith(), $share->getSharedBy()); 167 } catch (RoomNotFoundException $e) { 168 throw new GenericShareException('Room not found', $this->l->t('Conversation not found'), 404); 169 } 170 171 if ($room->getReadOnly() === Room::READ_ONLY) { 172 throw new GenericShareException('Room not found', $this->l->t('Conversation not found'), 404); 173 } 174 175 try { 176 $room->getParticipant($share->getSharedBy(), false); 177 } catch (ParticipantNotFoundException $e) { 178 // If the sharer is not a participant of the room even if the room 179 // exists the error is still "Room not found". 180 throw new GenericShareException('Room not found', $this->l->t('Conversation not found'), 404); 181 } 182 183 $existingShares = $this->getSharesByPath($share->getNode()); 184 foreach ($existingShares as $existingShare) { 185 if ($existingShare->getSharedWith() === $share->getSharedWith()) { 186 // FIXME Should be moved away from GenericEvent as soon as OCP\Share20\IManager did move too 187 $this->dispatcher->dispatch(self::class . '::' . 'share_file_again', new GenericEvent($existingShare)); 188 throw new GenericShareException('Already shared', $this->l->t('Path is already shared with this room'), 403); 189 } 190 } 191 192 $share->setToken( 193 $this->secureRandom->generate( 194 15, // \OC\Share\Constants::TOKEN_LENGTH 195 \OCP\Security\ISecureRandom::CHAR_HUMAN_READABLE 196 ) 197 ); 198 199 $shareId = $this->addShareToDB( 200 $share->getSharedWith(), 201 $share->getSharedBy(), 202 $share->getShareOwner(), 203 $share->getNodeType(), 204 $share->getNodeId(), 205 $share->getTarget(), 206 $share->getPermissions(), 207 $share->getToken(), 208 $share->getExpirationDate() 209 ); 210 211 $data = $this->getRawShare($shareId); 212 213 return $this->createShareObject($data); 214 } 215 216 /** 217 * Add share to the database and return the ID 218 * 219 * @param string $shareWith 220 * @param string $sharedBy 221 * @param string $shareOwner 222 * @param string $itemType 223 * @param int $itemSource 224 * @param string $target 225 * @param int $permissions 226 * @param string $token 227 * @param \DateTime|null $expirationDate 228 * @return int 229 */ 230 private function addShareToDB( 231 string $shareWith, 232 string $sharedBy, 233 string $shareOwner, 234 string $itemType, 235 int $itemSource, 236 string $target, 237 int $permissions, 238 string $token, 239 ?\DateTime $expirationDate 240 ): int { 241 $insert = $this->dbConnection->getQueryBuilder(); 242 $insert->insert('share') 243 ->setValue('share_type', $insert->createNamedParameter(IShare::TYPE_ROOM)) 244 ->setValue('share_with', $insert->createNamedParameter($shareWith)) 245 ->setValue('uid_initiator', $insert->createNamedParameter($sharedBy)) 246 ->setValue('uid_owner', $insert->createNamedParameter($shareOwner)) 247 ->setValue('item_type', $insert->createNamedParameter($itemType)) 248 ->setValue('item_source', $insert->createNamedParameter($itemSource)) 249 ->setValue('file_source', $insert->createNamedParameter($itemSource)) 250 ->setValue('file_target', $insert->createNamedParameter($target)) 251 ->setValue('permissions', $insert->createNamedParameter($permissions)) 252 ->setValue('token', $insert->createNamedParameter($token)) 253 ->setValue('stime', $insert->createNamedParameter($this->timeFactory->getTime())); 254 255 if ($expirationDate !== null) { 256 $insert->setValue('expiration', $insert->createNamedParameter($expirationDate, 'datetime')); 257 } 258 259 $insert->executeStatement(); 260 $id = $insert->getLastInsertId(); 261 262 return $id; 263 } 264 265 /** 266 * Get database row of the given share 267 * 268 * @param int $id 269 * @return array 270 * @throws ShareNotFound 271 */ 272 private function getRawShare(int $id): array { 273 $qb = $this->dbConnection->getQueryBuilder(); 274 $qb->select('*') 275 ->from('share') 276 ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))); 277 278 $cursor = $qb->executeQuery(); 279 $data = $cursor->fetch(); 280 $cursor->closeCursor(); 281 282 if ($data === false) { 283 throw new ShareNotFound(); 284 } 285 286 return $data; 287 } 288 289 /** 290 * Create a share object from a database row 291 * 292 * @param array $data 293 * @return IShare 294 */ 295 private function createShareObject(array $data): IShare { 296 $share = $this->shareManager->newShare(); 297 $share->setId((int)$data['id']) 298 ->setShareType((int)$data['share_type']) 299 ->setPermissions((int)$data['permissions']) 300 ->setTarget($data['file_target']) 301 ->setStatus((int)$data['accepted']) 302 ->setToken($data['token']); 303 304 $shareTime = $this->timeFactory->getDateTime(); 305 $shareTime->setTimestamp((int)$data['stime']); 306 $share->setShareTime($shareTime); 307 $share->setSharedWith($data['share_with']); 308 309 $share->setSharedBy($data['uid_initiator']); 310 $share->setShareOwner($data['uid_owner']); 311 312 if ($data['expiration'] !== null) { 313 $expiration = \DateTime::createFromFormat('Y-m-d H:i:s', $data['expiration']); 314 if ($expiration !== false) { 315 $share->setExpirationDate($expiration); 316 } 317 } 318 319 $share->setNodeId((int)$data['file_source']); 320 $share->setNodeType($data['item_type']); 321 322 $share->setProviderId($this->identifier()); 323 324 if (isset($data['f_permissions'])) { 325 $entryData = $data; 326 $entryData['permissions'] = $entryData['f_permissions']; 327 $entryData['parent'] = $entryData['f_parent']; 328 $share->setNodeCacheEntry(Cache::cacheEntryFromData($entryData, 329 $this->mimeTypeLoader)); 330 } 331 332 return $share; 333 } 334 335 /** 336 * Update a share 337 * 338 * @param IShare $share 339 * @return IShare The share object 340 */ 341 public function update(IShare $share): IShare { 342 $update = $this->dbConnection->getQueryBuilder(); 343 $update->update('share') 344 ->where($update->expr()->eq('id', $update->createNamedParameter($share->getId()))) 345 ->set('uid_owner', $update->createNamedParameter($share->getShareOwner())) 346 ->set('uid_initiator', $update->createNamedParameter($share->getSharedBy())) 347 ->set('permissions', $update->createNamedParameter($share->getPermissions())) 348 ->set('item_source', $update->createNamedParameter($share->getNode()->getId())) 349 ->set('file_source', $update->createNamedParameter($share->getNode()->getId())) 350 ->set('expiration', $update->createNamedParameter($share->getExpirationDate(), IQueryBuilder::PARAM_DATE)) 351 ->executeStatement(); 352 353 /* 354 * Update all user defined group shares 355 */ 356 $update = $this->dbConnection->getQueryBuilder(); 357 $update->update('share') 358 ->where($update->expr()->eq('parent', $update->createNamedParameter($share->getId()))) 359 ->set('uid_owner', $update->createNamedParameter($share->getShareOwner())) 360 ->set('uid_initiator', $update->createNamedParameter($share->getSharedBy())) 361 ->set('item_source', $update->createNamedParameter($share->getNode()->getId())) 362 ->set('file_source', $update->createNamedParameter($share->getNode()->getId())) 363 ->set('expiration', $update->createNamedParameter($share->getExpirationDate(), IQueryBuilder::PARAM_DATE)) 364 ->executeStatement(); 365 366 /* 367 * Now update the permissions for all children that have not set it to 0 368 */ 369 $update = $this->dbConnection->getQueryBuilder(); 370 $update->update('share') 371 ->where($update->expr()->eq('parent', $update->createNamedParameter($share->getId()))) 372 ->andWhere($update->expr()->neq('permissions', $update->createNamedParameter(0))) 373 ->set('permissions', $update->createNamedParameter($share->getPermissions())) 374 ->executeStatement(); 375 376 return $share; 377 } 378 379 /** 380 * Delete a share 381 * 382 * @param IShare $share 383 */ 384 public function delete(IShare $share): void { 385 $delete = $this->dbConnection->getQueryBuilder(); 386 $delete->delete('share') 387 ->where($delete->expr()->eq('id', $delete->createNamedParameter($share->getId()))); 388 389 $delete->orWhere($delete->expr()->eq('parent', $delete->createNamedParameter($share->getId()))); 390 391 $delete->executeStatement(); 392 } 393 394 /** 395 * Unshare a file from self as recipient. 396 * 397 * If a user unshares a room share from their self then the original room 398 * share should still exist. 399 * 400 * @param IShare $share 401 * @param string $recipient UserId of the recipient 402 */ 403 public function deleteFromSelf(IShare $share, $recipient): void { 404 // Check if there is a userroom share 405 $qb = $this->dbConnection->getQueryBuilder(); 406 $stmt = $qb->select(['id', 'permissions']) 407 ->from('share') 408 ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_USERROOM))) 409 ->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($recipient))) 410 ->andWhere($qb->expr()->eq('parent', $qb->createNamedParameter($share->getId()))) 411 ->andWhere($qb->expr()->orX( 412 $qb->expr()->eq('item_type', $qb->createNamedParameter('file')), 413 $qb->expr()->eq('item_type', $qb->createNamedParameter('folder')) 414 )) 415 ->executeQuery(); 416 417 $data = $stmt->fetch(); 418 $stmt->closeCursor(); 419 420 if ($data === false) { 421 // No userroom share yet. Create one. 422 $qb = $this->dbConnection->getQueryBuilder(); 423 $qb->insert('share') 424 ->values([ 425 'share_type' => $qb->createNamedParameter(self::SHARE_TYPE_USERROOM), 426 'share_with' => $qb->createNamedParameter($recipient), 427 'uid_owner' => $qb->createNamedParameter($share->getShareOwner()), 428 'uid_initiator' => $qb->createNamedParameter($share->getSharedBy()), 429 'parent' => $qb->createNamedParameter($share->getId()), 430 'item_type' => $qb->createNamedParameter($share->getNodeType()), 431 'item_source' => $qb->createNamedParameter($share->getNodeId()), 432 'file_source' => $qb->createNamedParameter($share->getNodeId()), 433 'file_target' => $qb->createNamedParameter($share->getTarget()), 434 'permissions' => $qb->createNamedParameter(0), 435 'stime' => $qb->createNamedParameter($share->getShareTime()->getTimestamp()), 436 ])->executeStatement(); 437 } elseif ($data['permissions'] !== 0) { 438 // Already a userroom share. Update it. 439 $qb = $this->dbConnection->getQueryBuilder(); 440 $qb->update('share') 441 ->set('permissions', $qb->createNamedParameter(0)) 442 ->where($qb->expr()->eq('id', $qb->createNamedParameter($data['id']))) 443 ->executeStatement(); 444 } 445 } 446 447 /** 448 * Restore a share for a given recipient. The implementation could be provider independant. 449 * 450 * @param IShare $share 451 * @param string $recipient 452 * @return IShare The restored share object 453 * @throws GenericShareException In case the share could not be restored 454 */ 455 public function restore(IShare $share, string $recipient): IShare { 456 $qb = $this->dbConnection->getQueryBuilder(); 457 $qb->select('permissions') 458 ->from('share') 459 ->where( 460 $qb->expr()->eq('id', $qb->createNamedParameter($share->getId())) 461 ); 462 $cursor = $qb->executeQuery(); 463 $data = $cursor->fetch(); 464 $cursor->closeCursor(); 465 466 $originalPermission = $data['permissions']; 467 468 $update = $this->dbConnection->getQueryBuilder(); 469 $update->update('share') 470 ->set('permissions', $update->createNamedParameter($originalPermission)) 471 ->where( 472 $update->expr()->eq('parent', $update->createNamedParameter($share->getId())) 473 )->andWhere( 474 $update->expr()->eq('share_type', $update->createNamedParameter(self::SHARE_TYPE_USERROOM)) 475 )->andWhere( 476 $update->expr()->eq('share_with', $update->createNamedParameter($recipient)) 477 ); 478 479 $update->executeStatement(); 480 481 return $this->getShareById($share->getId(), $recipient); 482 } 483 484 /** 485 * Move a share as a recipient. 486 * 487 * This is updating the share target. Thus the mount point of the recipient. 488 * This may require special handling. If a user moves a room share 489 * the target should only be changed for them. 490 * 491 * @param IShare $share 492 * @param string $recipient userId of recipient 493 * @return IShare 494 */ 495 public function move(IShare $share, $recipient): IShare { 496 // Check if there is a userroom share 497 $qb = $this->dbConnection->getQueryBuilder(); 498 $stmt = $qb->select('id') 499 ->from('share') 500 ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_USERROOM))) 501 ->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($recipient))) 502 ->andWhere($qb->expr()->eq('parent', $qb->createNamedParameter($share->getId()))) 503 ->andWhere($qb->expr()->orX( 504 $qb->expr()->eq('item_type', $qb->createNamedParameter('file')), 505 $qb->expr()->eq('item_type', $qb->createNamedParameter('folder')) 506 )) 507 ->setMaxResults(1) 508 ->executeQuery(); 509 510 $data = $stmt->fetch(); 511 $stmt->closeCursor(); 512 513 if ($data === false) { 514 // No userroom share yet. Create one. 515 $qb = $this->dbConnection->getQueryBuilder(); 516 $qb->insert('share') 517 ->values([ 518 'share_type' => $qb->createNamedParameter(self::SHARE_TYPE_USERROOM), 519 'share_with' => $qb->createNamedParameter($recipient), 520 'uid_owner' => $qb->createNamedParameter($share->getShareOwner()), 521 'uid_initiator' => $qb->createNamedParameter($share->getSharedBy()), 522 'parent' => $qb->createNamedParameter($share->getId()), 523 'item_type' => $qb->createNamedParameter($share->getNodeType()), 524 'item_source' => $qb->createNamedParameter($share->getNodeId()), 525 'file_source' => $qb->createNamedParameter($share->getNodeId()), 526 'file_target' => $qb->createNamedParameter($share->getTarget()), 527 'permissions' => $qb->createNamedParameter($share->getPermissions()), 528 'stime' => $qb->createNamedParameter($share->getShareTime()->getTimestamp()), 529 ])->executeStatement(); 530 } else { 531 // Already a userroom share. Update it. 532 $qb = $this->dbConnection->getQueryBuilder(); 533 $qb->update('share') 534 ->set('file_target', $qb->createNamedParameter($share->getTarget())) 535 ->where($qb->expr()->eq('id', $qb->createNamedParameter($data['id']))) 536 ->executeStatement(); 537 } 538 539 return $share; 540 } 541 542 /** 543 * Get all shares by the given user in a folder 544 * 545 * @param string $userId 546 * @param Folder $node 547 * @param bool $reshares Also get the shares where $user is the owner instead of just the shares where $user is the initiator 548 * @return IShare[][] 549 * @psalm-return array<array-key, non-empty-list<IShare>> 550 */ 551 public function getSharesInFolder($userId, Folder $node, $reshares): array { 552 $qb = $this->dbConnection->getQueryBuilder(); 553 $qb->select('*') 554 ->from('share', 's') 555 ->andWhere($qb->expr()->orX( 556 $qb->expr()->eq('s.item_type', $qb->createNamedParameter('file')), 557 $qb->expr()->eq('s.item_type', $qb->createNamedParameter('folder')) 558 )) 559 ->andWhere( 560 $qb->expr()->eq('s.share_type', $qb->createNamedParameter(IShare::TYPE_ROOM)) 561 ); 562 563 /** 564 * Reshares for this user are shares where they are the owner. 565 */ 566 if ($reshares === false) { 567 $qb->andWhere($qb->expr()->eq('s.uid_initiator', $qb->createNamedParameter($userId))); 568 } else { 569 $qb->andWhere( 570 $qb->expr()->orX( 571 $qb->expr()->eq('s.uid_owner', $qb->createNamedParameter($userId)), 572 $qb->expr()->eq('s.uid_initiator', $qb->createNamedParameter($userId)) 573 ) 574 ); 575 } 576 577 $qb->innerJoin('s', 'filecache' ,'f', $qb->expr()->eq('s.file_source', 'f.fileid')); 578 $qb->andWhere($qb->expr()->eq('f.parent', $qb->createNamedParameter($node->getId()))); 579 580 $qb->orderBy('s.id'); 581 582 $cursor = $qb->executeQuery(); 583 $shares = []; 584 while ($data = $cursor->fetch()) { 585 $shares[$data['fileid']][] = $this->createShareObject($data); 586 } 587 $cursor->closeCursor(); 588 589 return $shares; 590 } 591 592 /** 593 * Get all shares by the given user 594 * 595 * @param string $userId 596 * @param int $shareType 597 * @param Node|null $node 598 * @param bool $reshares Also get the shares where $user is the owner instead of just the shares where $user is the initiator 599 * @param int $limit The maximum number of shares to be returned, -1 for all shares 600 * @param int $offset 601 * @return IShare[] 602 */ 603 public function getSharesBy($userId, $shareType, $node, $reshares, $limit, $offset): array { 604 $qb = $this->dbConnection->getQueryBuilder(); 605 $qb->select('*') 606 ->from('share'); 607 608 $qb->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_ROOM))); 609 610 /** 611 * Reshares for this user are shares where they are the owner. 612 */ 613 if ($reshares === false) { 614 $qb->andWhere($qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId))); 615 } else { 616 $qb->andWhere( 617 $qb->expr()->orX( 618 $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)), 619 $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)) 620 ) 621 ); 622 } 623 624 if ($node !== null) { 625 $qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId()))); 626 } 627 628 if ($limit !== -1) { 629 $qb->setMaxResults($limit); 630 } 631 632 $qb->setFirstResult($offset); 633 $qb->orderBy('id'); 634 635 $cursor = $qb->executeQuery(); 636 $shares = []; 637 while ($data = $cursor->fetch()) { 638 $shares[] = $this->createShareObject($data); 639 } 640 $cursor->closeCursor(); 641 642 return $shares; 643 } 644 645 /** 646 * Get share by id 647 * 648 * @param int $id 649 * @param string|null $recipientId 650 * @return IShare 651 * @throws ShareNotFound 652 */ 653 public function getShareById($id, $recipientId = null): IShare { 654 $qb = $this->dbConnection->getQueryBuilder(); 655 $qb->select('s.*', 656 'f.fileid', 'f.path', 'f.permissions AS f_permissions', 'f.storage', 'f.path_hash', 657 'f.parent AS f_parent', 'f.name', 'f.mimetype', 'f.mimepart', 'f.size', 'f.mtime', 'f.storage_mtime', 658 'f.encrypted', 'f.unencrypted_size', 'f.etag', 'f.checksum' 659 ) 660 ->selectAlias('st.id', 'storage_string_id') 661 ->from('share', 's') 662 ->leftJoin('s', 'filecache', 'f', $qb->expr()->eq('s.file_source', 'f.fileid')) 663 ->leftJoin('f', 'storages', 'st', $qb->expr()->eq('f.storage', 'st.numeric_id')) 664 ->where($qb->expr()->eq('s.id', $qb->createNamedParameter($id))) 665 ->andWhere($qb->expr()->eq('s.share_type', $qb->createNamedParameter(IShare::TYPE_ROOM))); 666 667 $cursor = $qb->executeQuery(); 668 $data = $cursor->fetch(); 669 $cursor->closeCursor(); 670 671 if ($data === false) { 672 throw new ShareNotFound(); 673 } 674 675 if (!$this->isAccessibleResult($data)) { 676 throw new ShareNotFound(); 677 } 678 679 $share = $this->createShareObject($data); 680 681 if ($recipientId !== null) { 682 $share = $this->resolveSharesForRecipient([$share], $recipientId)[0]; 683 } 684 685 return $share; 686 } 687 688 /** 689 * Returns each given share as seen by the given recipient. 690 * 691 * If the recipient has not modified the share the original one is returned 692 * instead. 693 * 694 * @param IShare[] $shares 695 * @param string $userId 696 * @return IShare[] 697 */ 698 private function resolveSharesForRecipient(array $shares, string $userId): array { 699 $result = []; 700 701 $start = 0; 702 while (true) { 703 /** @var IShare[] $shareSlice */ 704 $shareSlice = array_slice($shares, $start, 100); 705 $start += 100; 706 707 if ($shareSlice === []) { 708 break; 709 } 710 711 /** @var int[] $ids */ 712 $ids = []; 713 /** @var IShare[] $shareMap */ 714 $shareMap = []; 715 716 foreach ($shareSlice as $share) { 717 $ids[] = (int)$share->getId(); 718 $shareMap[$share->getId()] = $share; 719 } 720 721 $qb = $this->dbConnection->getQueryBuilder(); 722 723 $query = $qb->select('*') 724 ->from('share') 725 ->where($qb->expr()->in('parent', $qb->createNamedParameter($ids, IQueryBuilder::PARAM_INT_ARRAY))) 726 ->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($userId))) 727 ->andWhere($qb->expr()->orX( 728 $qb->expr()->eq('item_type', $qb->createNamedParameter('file')), 729 $qb->expr()->eq('item_type', $qb->createNamedParameter('folder')) 730 )); 731 732 $stmt = $query->executeQuery(); 733 734 while ($data = $stmt->fetch()) { 735 $shareMap[$data['parent']]->setPermissions((int)$data['permissions']); 736 $shareMap[$data['parent']]->setTarget($data['file_target']); 737 } 738 739 $stmt->closeCursor(); 740 741 foreach ($shareMap as $share) { 742 $result[] = $share; 743 } 744 } 745 746 return $result; 747 } 748 749 /** 750 * Get shares for a given path 751 * 752 * @param Node $path 753 * @return IShare[] 754 */ 755 public function getSharesByPath(Node $path): array { 756 $qb = $this->dbConnection->getQueryBuilder(); 757 758 $cursor = $qb->select('*') 759 ->from('share') 760 ->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($path->getId()))) 761 ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_ROOM))) 762 ->executeQuery(); 763 764 $shares = []; 765 while ($data = $cursor->fetch()) { 766 $shares[] = $this->createShareObject($data); 767 } 768 $cursor->closeCursor(); 769 770 return $shares; 771 } 772 773 /** 774 * Get shared with the given user 775 * 776 * @param string $userId get shares where this user is the recipient 777 * @param int $shareType 778 * @param Node|null $node 779 * @param int $limit The max number of entries returned, -1 for all 780 * @param int $offset 781 * @return IShare[] 782 */ 783 public function getSharedWith($userId, $shareType, $node, $limit, $offset): array { 784 $allRooms = $this->manager->getRoomTokensForUser($userId); 785 786 /** @var IShare[] $shares */ 787 $shares = []; 788 789 $start = 0; 790 while (true) { 791 $rooms = array_slice($allRooms, $start, 100); 792 $start += 100; 793 794 if ($rooms === []) { 795 break; 796 } 797 798 $qb = $this->dbConnection->getQueryBuilder(); 799 $qb->select('s.*', 800 'f.fileid', 'f.path', 'f.permissions AS f_permissions', 'f.storage', 'f.path_hash', 801 'f.parent AS f_parent', 'f.name', 'f.mimetype', 'f.mimepart', 'f.size', 'f.mtime', 'f.storage_mtime', 802 'f.encrypted', 'f.unencrypted_size', 'f.etag', 'f.checksum' 803 ) 804 ->selectAlias('st.id', 'storage_string_id') 805 ->from('share', 's') 806 ->orderBy('s.id') 807 ->leftJoin('s', 'filecache', 'f', $qb->expr()->eq('s.file_source', 'f.fileid')) 808 ->leftJoin('f', 'storages', 'st', $qb->expr()->eq('f.storage', 'st.numeric_id')); 809 810 if ($limit !== -1) { 811 $qb->setMaxResults($limit); 812 } 813 814 // Filter by node if provided 815 if ($node !== null) { 816 $qb->andWhere($qb->expr()->eq('s.file_source', $qb->createNamedParameter($node->getId()))); 817 } 818 819 $qb->andWhere($qb->expr()->eq('s.share_type', $qb->createNamedParameter(IShare::TYPE_ROOM))) 820 ->andWhere($qb->expr()->in('s.share_with', $qb->createNamedParameter( 821 $rooms, 822 IQueryBuilder::PARAM_STR_ARRAY 823 ))) 824 ->andWhere($qb->expr()->orX( 825 $qb->expr()->eq('s.item_type', $qb->createNamedParameter('file')), 826 $qb->expr()->eq('s.item_type', $qb->createNamedParameter('folder')) 827 )); 828 829 $cursor = $qb->executeQuery(); 830 while ($data = $cursor->fetch()) { 831 if (!$this->isAccessibleResult($data)) { 832 continue; 833 } 834 835 if ($offset > 0) { 836 $offset--; 837 continue; 838 } 839 840 $shares[] = $this->createShareObject($data); 841 } 842 $cursor->closeCursor(); 843 } 844 845 $shares = $this->resolveSharesForRecipient($shares, $userId); 846 847 return $shares; 848 } 849 850 private function isAccessibleResult(array $data): bool { 851 // exclude shares leading to deleted file entries 852 if ($data['fileid'] === null || $data['path'] === null) { 853 return false; 854 } 855 856 // exclude shares leading to trashbin on home storages 857 $pathSections = explode('/', $data['path'], 2); 858 // FIXME: would not detect rare md5'd home storage case properly 859 if ($pathSections[0] !== 'files' 860 && in_array(explode(':', $data['storage_string_id'], 2)[0], ['home', 'object'])) { 861 return false; 862 } 863 return true; 864 } 865 866 /** 867 * Get a share by token 868 * 869 * Note that token here refers to share token, not room token. 870 * 871 * @param string $token 872 * @return IShare 873 * @throws ShareNotFound 874 */ 875 public function getShareByToken($token): IShare { 876 $qb = $this->dbConnection->getQueryBuilder(); 877 878 $cursor = $qb->select('*') 879 ->from('share') 880 ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_ROOM))) 881 ->andWhere($qb->expr()->eq('token', $qb->createNamedParameter($token))) 882 ->executeQuery(); 883 884 $data = $cursor->fetch(); 885 886 if ($data === false) { 887 throw new ShareNotFound(); 888 } 889 890 $roomToken = $data['share_with']; 891 try { 892 $room = $this->manager->getRoomByToken($roomToken); 893 } catch (RoomNotFoundException $e) { 894 throw new ShareNotFound(); 895 } 896 897 if ($room->getType() !== Room::TYPE_PUBLIC) { 898 throw new ShareNotFound(); 899 } 900 901 return $this->createShareObject($data); 902 } 903 904 /** 905 * A user is deleted from the system 906 * So clean up the relevant shares. 907 * 908 * @param string $uid 909 * @param int $shareType 910 */ 911 public function userDeleted($uid, $shareType): void { 912 // A deleted user is handled automatically by the room hooks due to the 913 // user being removed from the room. 914 } 915 916 /** 917 * A group is deleted from the system. 918 * We have to clean up all shares to this group. 919 * Providers not handling group shares should just return 920 * 921 * @param string $gid 922 */ 923 public function groupDeleted($gid): void { 924 } 925 926 /** 927 * A user is deleted from a group 928 * We have to clean up all the related user specific group shares 929 * Providers not handling group shares should just return 930 * 931 * @param string $uid 932 * @param string $gid 933 */ 934 public function userDeletedFromGroup($uid, $gid): void { 935 } 936 937 /** 938 * Get the access list to the array of provided nodes. 939 * 940 * @see IManager::getAccessList() for sample docs 941 * 942 * @param Node[] $nodes The list of nodes to get access for 943 * @param bool $currentAccess If current access is required (like for removed shares that might get revived later) 944 * @return array 945 */ 946 public function getAccessList($nodes, $currentAccess): array { 947 $ids = []; 948 foreach ($nodes as $node) { 949 $ids[] = $node->getId(); 950 } 951 952 $qb = $this->dbConnection->getQueryBuilder(); 953 954 $types = [IShare::TYPE_ROOM]; 955 if ($currentAccess) { 956 $types[] = self::SHARE_TYPE_USERROOM; 957 } 958 959 $qb->select('id', 'parent', 'share_type', 'share_with', 'file_source', 'file_target', 'permissions') 960 ->from('share') 961 ->where($qb->expr()->in('share_type', $qb->createNamedParameter($types, IQueryBuilder::PARAM_INT_ARRAY))) 962 ->andWhere($qb->expr()->in('file_source', $qb->createNamedParameter($ids, IQueryBuilder::PARAM_INT_ARRAY))) 963 ->andWhere($qb->expr()->orX( 964 $qb->expr()->eq('item_type', $qb->createNamedParameter('file')), 965 $qb->expr()->eq('item_type', $qb->createNamedParameter('folder')) 966 )); 967 $cursor = $qb->executeQuery(); 968 969 $users = []; 970 while ($row = $cursor->fetch()) { 971 $type = (int)$row['share_type']; 972 if ($type === IShare::TYPE_ROOM) { 973 $roomToken = $row['share_with']; 974 try { 975 $room = $this->manager->getRoomByToken($roomToken); 976 } catch (RoomNotFoundException $e) { 977 continue; 978 } 979 980 $userList = $this->participantService->getParticipantUserIds($room); 981 foreach ($userList as $uid) { 982 $users[$uid] = $users[$uid] ?? []; 983 $users[$uid][$row['id']] = $row; 984 } 985 } elseif ($type === self::SHARE_TYPE_USERROOM && $currentAccess === true) { 986 $uid = $row['share_with']; 987 $users[$uid] = $users[$uid] ?? []; 988 $users[$uid][$row['id']] = $row; 989 } 990 } 991 $cursor->closeCursor(); 992 993 if ($currentAccess === true) { 994 $users = array_map([$this, 'filterSharesOfUser'], $users); 995 $users = array_filter($users); 996 } else { 997 $users = array_keys($users); 998 } 999 1000 return ['users' => $users]; 1001 } 1002 1003 /** 1004 * For each user the path with the fewest slashes is returned 1005 * @param array $shares 1006 * @return array 1007 */ 1008 protected function filterSharesOfUser(array $shares): array { 1009 // Room shares when the user has a share exception 1010 foreach ($shares as $id => $share) { 1011 $type = (int) $share['share_type']; 1012 $permissions = (int) $share['permissions']; 1013 1014 if ($type === self::SHARE_TYPE_USERROOM) { 1015 unset($shares[$share['parent']]); 1016 1017 if ($permissions === 0) { 1018 unset($shares[$id]); 1019 } 1020 } 1021 } 1022 1023 $best = []; 1024 $bestDepth = 0; 1025 foreach ($shares as $id => $share) { 1026 $depth = substr_count($share['file_target'], '/'); 1027 if (empty($best) || $depth < $bestDepth) { 1028 $bestDepth = $depth; 1029 $best = [ 1030 'node_id' => $share['file_source'], 1031 'node_path' => $share['file_target'], 1032 ]; 1033 } 1034 } 1035 1036 return $best; 1037 } 1038 1039 /** 1040 * Get all children of this share 1041 * 1042 * Not part of IShareProvider API, but needed by OC\Share20\Manager. 1043 * 1044 * @param IShare $parent 1045 * @return IShare[] 1046 */ 1047 public function getChildren(IShare $parent): array { 1048 $children = []; 1049 1050 $qb = $this->dbConnection->getQueryBuilder(); 1051 $qb->select('*') 1052 ->from('share') 1053 ->where($qb->expr()->eq('parent', $qb->createNamedParameter($parent->getId()))) 1054 ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_ROOM))) 1055 ->orderBy('id'); 1056 1057 $cursor = $qb->executeQuery(); 1058 while ($data = $cursor->fetch()) { 1059 $children[] = $this->createShareObject($data); 1060 } 1061 $cursor->closeCursor(); 1062 1063 return $children; 1064 } 1065 1066 /** 1067 * Delete all shares in a room, or only those from the given user. 1068 * 1069 * When a user is given all her shares are removed, both own shares and 1070 * received shares. 1071 * 1072 * Not part of IShareProvider API, but needed by the hooks in 1073 * OCA\Talk\AppInfo\Application 1074 * 1075 * @param string $roomToken 1076 * @param string|null $user 1077 */ 1078 public function deleteInRoom(string $roomToken, string $user = null): void { 1079 //First delete all custom room shares for the original shares to be removed 1080 $qb = $this->dbConnection->getQueryBuilder(); 1081 $qb->select('id') 1082 ->from('share') 1083 ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_ROOM))) 1084 ->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($roomToken))); 1085 1086 if ($user !== null) { 1087 $qb->andWhere($qb->expr()->eq('uid_initiator', $qb->createNamedParameter($user))); 1088 } 1089 1090 $cursor = $qb->executeQuery(); 1091 $ids = []; 1092 while ($row = $cursor->fetch()) { 1093 $ids[] = (int)$row['id']; 1094 } 1095 $cursor->closeCursor(); 1096 1097 if (!empty($ids)) { 1098 $chunks = array_chunk($ids, 100); 1099 foreach ($chunks as $chunk) { 1100 $qb->delete('share') 1101 ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_USERROOM))) 1102 ->andWhere($qb->expr()->in('parent', $qb->createNamedParameter($chunk, IQueryBuilder::PARAM_INT_ARRAY))); 1103 $qb->executeStatement(); 1104 } 1105 } 1106 1107 // Now delete all the original room shares 1108 $delete = $this->dbConnection->getQueryBuilder(); 1109 $delete->delete('share') 1110 ->where($delete->expr()->eq('share_type', $delete->createNamedParameter(IShare::TYPE_ROOM))) 1111 ->andWhere($delete->expr()->eq('share_with', $delete->createNamedParameter($roomToken))); 1112 1113 if ($user !== null) { 1114 $delete->andWhere($delete->expr()->eq('uid_initiator', $delete->createNamedParameter($user))); 1115 } 1116 1117 $delete->executeStatement(); 1118 1119 // Finally delete all custom room shares leftovers for the given user 1120 if ($user !== null) { 1121 $query = $this->dbConnection->getQueryBuilder(); 1122 $query->select('id') 1123 ->from('share') 1124 ->where($query->expr()->eq('share_type', $query->createNamedParameter(IShare::TYPE_ROOM))) 1125 ->andWhere($query->expr()->eq('share_with', $query->createNamedParameter($roomToken))); 1126 1127 $cursor = $query->executeQuery(); 1128 $ids = []; 1129 while ($row = $cursor->fetch()) { 1130 $ids[] = (int)$row['id']; 1131 } 1132 $cursor->closeCursor(); 1133 1134 if (!empty($ids)) { 1135 $chunks = array_chunk($ids, 100); 1136 foreach ($chunks as $chunk) { 1137 $delete->delete('share') 1138 ->where($delete->expr()->eq('share_type', $delete->createNamedParameter(self::SHARE_TYPE_USERROOM))) 1139 ->andWhere($delete->expr()->in('share_with', $delete->createNamedParameter($user))) 1140 ->andWhere($delete->expr()->in('parent', $delete->createNamedParameter($chunk, IQueryBuilder::PARAM_INT_ARRAY))); 1141 $delete->executeStatement(); 1142 } 1143 } 1144 } 1145 } 1146 1147 /** 1148 * Get all the shares in this provider returned as iterable to reduce memory 1149 * overhead 1150 * 1151 * @return iterable 1152 * @since 18.0.0 1153 */ 1154 public function getAllShares(): iterable { 1155 $qb = $this->dbConnection->getQueryBuilder(); 1156 1157 $qb->select('*') 1158 ->from('share') 1159 ->where( 1160 $qb->expr()->orX( 1161 $qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_ROOM)) 1162 ) 1163 ); 1164 1165 $cursor = $qb->executeQuery(); 1166 while ($data = $cursor->fetch()) { 1167 $share = $this->createShareObject($data); 1168 1169 yield $share; 1170 } 1171 $cursor->closeCursor(); 1172 } 1173} 1174