diff --git a/lib/Album/AlbumMapper.php b/lib/Album/AlbumMapper.php index 613562e49..14cedb2b5 100644 --- a/lib/Album/AlbumMapper.php +++ b/lib/Album/AlbumMapper.php @@ -273,6 +273,9 @@ public function removeFile(int $albumId, int $fileId): void { $query->executeStatement(); } + /** + * Remove all files added by a user from an album. + */ public function removeFilesForUser(int $albumId, string $userId) { // Remove all photos by this user from the album: $query = $this->connection->getQueryBuilder(); @@ -289,6 +292,36 @@ public function removeFilesForUser(int $albumId, string $userId) { ->executeStatement(); } + /** + * Remove a given file from any albums in which it was added by a given user. + */ + public function removeFileWithOwner(int $fileId, string $ownerId): void { + // Get concerned albums before deleting them. + $query = $this->connection->getQueryBuilder(); + $albumsRows = $query->select('album_id') + ->from("photos_albums_files") + ->where($query->expr()->eq("owner_id", $query->createNamedParameter($ownerId))) + ->andWhere($query->expr()->eq("file_id", $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT))) + ->executeQuery() + ->fetchAll(); + + // Remove any occurrence of fileId when owner is ownerId. + $query = $this->connection->getQueryBuilder(); + $query->delete("photos_albums_files") + ->where($query->expr()->eq("owner_id", $query->createNamedParameter($ownerId))) + ->andWhere($query->expr()->eq("file_id", $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT))) + ->executeStatement(); + + // Update last_added_photo for concerned albums. + foreach ($albumsRows as $row) { + $query = $this->connection->getQueryBuilder(); + $query->update("photos_albums") + ->set('last_added_photo', $query->createNamedParameter($this->getLastAdded($row['album_id']), IQueryBuilder::PARAM_INT)) + ->where($query->expr()->eq('album_id', $query->createNamedParameter($row['album_id'], IQueryBuilder::PARAM_INT))); + $query->executeStatement(); + } + } + private function getLastAdded(int $albumId): int { $query = $this->connection->getQueryBuilder(); $query->select("file_id") diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index f53f9450e..947137a4e 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -28,11 +28,9 @@ use OCA\DAV\Events\SabrePluginAuthInitEvent; use OCA\Photos\Listener\SabrePluginAuthInitListener; use OCA\DAV\Connector\Sabre\Principal; -use OCA\Photos\Listener\NodeDeletedListener; use OCA\Photos\Listener\TagListener; -use OCA\Photos\Listener\GroupUserRemovedListener; -use OCA\Photos\Listener\GroupDeletedListener; use OCA\Photos\Listener\PlaceManagerEventListener; +use OCA\Photos\Listener\AlbumsManagementEventListener; use OCP\AppFramework\App; use OCP\AppFramework\Bootstrap\IBootContext; use OCP\AppFramework\Bootstrap\IBootstrap; @@ -42,6 +40,8 @@ use OCP\Group\Events\UserRemovedEvent; use OCP\Group\Events\GroupDeletedEvent; use OCP\Files\Events\Node\NodeWrittenEvent; +use OCP\Share\Events\ShareDeletedEvent; +use OCP\User\Events\UserDeletedEvent; class Application extends App implements IBootstrap { public const APP_ID = 'photos'; @@ -75,15 +75,15 @@ public function register(IRegistrationContext $context): void { /** Register $principalBackend for the DAV collection */ $context->registerServiceAlias('principalBackend', Principal::class); - $context->registerEventListener(NodeDeletedEvent::class, NodeDeletedListener::class); - - $context->registerEventListener(UserRemovedEvent::class, GroupUserRemovedListener::class); - - $context->registerEventListener(GroupDeletedEvent::class, GroupDeletedListener::class); - // Priority of -1 to be triggered after event listeners populating metadata. $context->registerEventListener(NodeWrittenEvent::class, PlaceManagerEventListener::class, -1); + $context->registerEventListener(NodeDeletedEvent::class, AlbumsManagementEventListener::class); + $context->registerEventListener(UserRemovedEvent::class, AlbumsManagementEventListener::class); + $context->registerEventListener(GroupDeletedEvent::class, AlbumsManagementEventListener::class); + $context->registerEventListener(UserDeletedEvent::class, AlbumsManagementEventListener::class); + $context->registerEventListener(ShareDeletedEvent::class, AlbumsManagementEventListener::class); + $context->registerEventListener(SabrePluginAuthInitEvent::class, SabrePluginAuthInitListener::class); $context->registerEventListener(MapperEvent::EVENT_ASSIGN, TagListener::class); diff --git a/lib/Listener/AlbumsManagementEventListener.php b/lib/Listener/AlbumsManagementEventListener.php new file mode 100644 index 000000000..949823676 --- /dev/null +++ b/lib/Listener/AlbumsManagementEventListener.php @@ -0,0 +1,108 @@ +albumMapper = $albumMapper; + $this->logger = $logger; + } + + public function handle(Event $event): void { + if ($event instanceof NodeDeletedEvent) { + try { + // Remove node from all albums containing it. + $albums = $this->albumMapper->getForFile($event->getNode()->getId()); + + foreach ($albums as $album) { + $this->albumMapper->removeFile($album->getId(), $event->getNode()->getId()); + } + } catch(\Throwable $ex) { + // If an error occur, return silently as we don't want to block the rest of the deletion process. + // It happened already during migrations when the albums table is not yet created, but a folder is deleted by the theming app. + $this->logger->error($ex->getMessage(), ['exception' => $ex]); + } + } elseif ($event instanceof UserDeletedEvent) { + // Delete all user's albums. + $albums = $this->albumMapper->getForUser($event->getUser()->getUID()); + foreach ($albums as $album) { + $this->albumMapper->delete($album->getId()); + } + } elseif ($event instanceof ShareDeletedEvent) { + $receiverId = $event->getShare()->getSharedWith(); + $this->forEachSubNode( + $event->getShare()->getNode(), + // Remove node from any album when the owner is $receiverId. + fn ($node) => $this->albumMapper->removeFileWithOwner($node->getId(), $receiverId), + ); + } elseif ($event instanceof UserRemovedEvent) { + // Get all shared albums for this group: + $albums_group = $this->albumMapper->getSharedAlbumsForCollaborator($event->getGroup()->getGID(), AlbumMapper::TYPE_GROUP); + // Get all albums shared with this specific user: + $albums_user = $this->albumMapper->getSharedAlbumsForCollaborator($event->getUser()->getUID(), AlbumMapper::TYPE_USER); + // Get all group-shared albums that are not directly shared with the removed user in addition + $albums = array_udiff($albums_group, $albums_user, fn ($a, $b) => ($a->getId() - $b->getId())); + + // Remove their photos from theses albums: + foreach ($albums as $album) { + $this->albumMapper->removeFilesForUser($album->getId(), $event->getUser()->getUID()); + } + } elseif ($event instanceof GroupDeletedEvent) { + // Get all shared albums for this group: + $albums_group = $this->albumMapper->getSharedAlbumsForCollaborator($event->getGroup()->getGID(), AlbumMapper::TYPE_GROUP); + + // Get all users of this group: + $users = $event->getGroup()->getUsers(); + + foreach ($users as $user) { + // Get all albums shared with this specific user: + $albums_user = $this->albumMapper->getSharedAlbumsForCollaborator($user->getUID(), AlbumMapper::TYPE_USER); + + // Get all group-shared albums that are not directly shared with the removed user in addition + $albums = array_udiff($albums_group, $albums_user, fn ($a, $b) => ($a->getId() - $b->getId())); + + // Remove their photos from theses albums: + foreach ($albums as $album) { + $this->albumMapper->removeFilesForUser($album->getId(), $user->getUID()); + } + } + + foreach ($albums_group as $album) { + $this->albumMapper->deleteGroupFromAlbumCollaboratorsList($event->getGroup()->getGID(), $album->getId()); + } + } + } + + private function forEachSubNode(Node $node, callable $callback): void { + if ($node instanceof Folder) { + foreach ($node->getDirectoryListing() as $subNode) { + $this->forEachSubNode($subNode, $callback); + } + } elseif ($node instanceof File) { + if (!str_starts_with($node->getMimeType(), 'image')) { + return; + } + + $callback($node); + } + } +} diff --git a/lib/Listener/GroupDeletedListener.php b/lib/Listener/GroupDeletedListener.php deleted file mode 100644 index 6a2f45c1e..000000000 --- a/lib/Listener/GroupDeletedListener.php +++ /dev/null @@ -1,47 +0,0 @@ -albumMapper = $albumMapper; - } - - public function handle(Event $event): void { - if (!($event instanceof GroupDeletedEvent)) { - return; - } - - // Get all shared albums for this group: - $albums_group = $this->albumMapper->getSharedAlbumsForCollaborator($event->getGroup()->getGID(), AlbumMapper::TYPE_GROUP); - - // Get all users of this group: - $users = $event->getGroup()->getUsers(); - - foreach ($users as $user) { - $uid = $user->getUID(); - - // Get all albums shared with this specific user: - $albums_user = $this->albumMapper->getSharedAlbumsForCollaborator($user->getUID(), AlbumMapper::TYPE_USER); - - // Get all group-shared albums that are not directly shared with the removed user in addition - $albums = array_udiff($albums_group, $albums_user, fn ($a, $b) => ($a->getId() - $b->getId())); - - // Remove their photos from theses albums: - foreach ($albums as $album) { - $this->albumMapper->removeFilesForUser($album->getId(), $user->getUID()); - } - } - - foreach ($albums_group as $album) { - $this->albumMapper->deleteGroupFromAlbumCollaboratorsList($event->getGroup()->getGID(), $album->getId()); - } - } -} diff --git a/lib/Listener/GroupUserRemovedListener.php b/lib/Listener/GroupUserRemovedListener.php deleted file mode 100644 index 141b55166..000000000 --- a/lib/Listener/GroupUserRemovedListener.php +++ /dev/null @@ -1,34 +0,0 @@ -albumMapper = $albumMapper; - } - - public function handle(Event $event): void { - if (!($event instanceof UserRemovedEvent)) { - return; - } - - // Get all shared albums for this group: - $albums_group = $this->albumMapper->getSharedAlbumsForCollaborator($event->getGroup()->getGID(), AlbumMapper::TYPE_GROUP); - // Get all albums shared with this specific user: - $albums_user = $this->albumMapper->getSharedAlbumsForCollaborator($event->getUser()->getUID(), AlbumMapper::TYPE_USER); - // Get all group-shared albums that are not directly shared with the removed user in addition - $albums = array_udiff($albums_group, $albums_user, fn ($a, $b) => ($a->getId() - $b->getId())); - - // Remove their photos from theses albums: - foreach ($albums as $album) { - $this->albumMapper->removeFilesForUser($album->getId(), $event->getUser()->getUID()); - } - } -} diff --git a/lib/Listener/NodeDeletedListener.php b/lib/Listener/NodeDeletedListener.php deleted file mode 100644 index 96bcdd2aa..000000000 --- a/lib/Listener/NodeDeletedListener.php +++ /dev/null @@ -1,34 +0,0 @@ -albumMapper = $albumMapper; - } - - public function handle(Event $event): void { - if (!($event instanceof NodeDeletedEvent)) { - return; - } - - try { - // Remove node from all albums containing it. - $albums = $this->albumMapper->getForFile($event->getNode()->getId()); - - foreach ($albums as $album) { - $this->albumMapper->removeFile($album->getId(), $event->getNode()->getId()); - } - } catch(\Throwable $ex) { - // If an error occur, return silently as we don't want to block the rest of the deletion process. - // It happened already during migrations when the albums table is not yet created, but a folder is deleted by the theming app. - } - } -}