diff options
Diffstat (limited to 'apps/dav/lib/Command')
20 files changed, 932 insertions, 40 deletions
diff --git a/apps/dav/lib/Command/ClearCalendarUnshares.php b/apps/dav/lib/Command/ClearCalendarUnshares.php new file mode 100644 index 00000000000..bb367a9cd0f --- /dev/null +++ b/apps/dav/lib/Command/ClearCalendarUnshares.php @@ -0,0 +1,114 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\Command; + +use OCA\DAV\CalDAV\CalDavBackend; +use OCA\DAV\CalDAV\Sharing\Backend; +use OCA\DAV\CalDAV\Sharing\Service; +use OCA\DAV\Connector\Sabre\Principal; +use OCA\DAV\DAV\Sharing\Backend as BackendAlias; +use OCA\DAV\DAV\Sharing\SharingMapper; +use OCP\IAppConfig; +use OCP\IUserManager; +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\QuestionHelper; +use Symfony\Component\Console\Helper\Table; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; + +#[AsCommand( + name: 'dav:clear-calendar-unshares', + description: 'Clear calendar unshares for a user', + hidden: false, +)] +class ClearCalendarUnshares extends Command { + public function __construct( + private IUserManager $userManager, + private IAppConfig $appConfig, + private Principal $principal, + private CalDavBackend $caldav, + private Backend $sharingBackend, + private Service $sharingService, + private SharingMapper $mapper, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this->addArgument( + 'uid', + InputArgument::REQUIRED, + 'User whose unshares to clear' + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $user = (string)$input->getArgument('uid'); + if (!$this->userManager->userExists($user)) { + throw new \InvalidArgumentException("User $user is unknown"); + } + + $principal = $this->principal->getPrincipalByPath('principals/users/' . $user); + if ($principal === null) { + throw new \InvalidArgumentException("Unable to fetch principal for user $user "); + } + + $shares = $this->mapper->getSharesByPrincipals([$principal['uri']], 'calendar'); + $unshares = array_filter($shares, static fn ($share) => $share['access'] === BackendAlias::ACCESS_UNSHARED); + + if (count($unshares) === 0) { + $output->writeln("User $user has no calendar unshares"); + return self::SUCCESS; + } + + $rows = array_map(fn ($share) => $this->formatCalendarUnshare($share), $shares); + + $table = new Table($output); + $table + ->setHeaders(['Share Id', 'Calendar Id', 'Calendar URI', 'Calendar Name']) + ->setRows($rows) + ->render(); + + $output->writeln(''); + + /** @var QuestionHelper $helper */ + $helper = $this->getHelper('question'); + $question = new ConfirmationQuestion('Please confirm to delete the above calendar unshare entries [y/n]', false); + + if ($helper->ask($input, $output, $question)) { + $this->mapper->deleteUnsharesByPrincipal($principal['uri'], 'calendar'); + $output->writeln("Calendar unshares for user $user deleted"); + } + + return self::SUCCESS; + } + + private function formatCalendarUnshare(array $share): array { + $calendarInfo = $this->caldav->getCalendarById($share['resourceid']); + + $resourceUri = 'Resource not found'; + $resourceName = ''; + + if ($calendarInfo !== null) { + $resourceUri = $calendarInfo['uri']; + $resourceName = $calendarInfo['{DAV:}displayname']; + } + + return [ + $share['id'], + $share['resourceid'], + $resourceUri, + $resourceName, + ]; + } +} diff --git a/apps/dav/lib/Command/ClearContactsPhotoCache.php b/apps/dav/lib/Command/ClearContactsPhotoCache.php new file mode 100644 index 00000000000..82e64c3145a --- /dev/null +++ b/apps/dav/lib/Command/ClearContactsPhotoCache.php @@ -0,0 +1,75 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\Command; + +use OCP\Files\AppData\IAppDataFactory; +use OCP\Files\NotPermittedException; +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\ProgressBar; +use Symfony\Component\Console\Helper\QuestionHelper; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; + +#[AsCommand( + name: 'dav:clear-contacts-photo-cache', + description: 'Clear cached contact photos', + hidden: false, +)] +class ClearContactsPhotoCache extends Command { + + public function __construct( + private IAppDataFactory $appDataFactory, + ) { + parent::__construct(); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $photoCacheAppData = $this->appDataFactory->get('dav-photocache'); + + $folders = $photoCacheAppData->getDirectoryListing(); + $countFolders = count($folders); + + if ($countFolders === 0) { + $output->writeln('No cached contact photos found.'); + return self::SUCCESS; + } + + $output->writeln('Found ' . count($folders) . ' cached contact photos.'); + + /** @var QuestionHelper $helper */ + $helper = $this->getHelper('question'); + $question = new ConfirmationQuestion('Please confirm to clear the contacts photo cache [y/n] ', true); + + if ($helper->ask($input, $output, $question) === false) { + $output->writeln('Clearing the contacts photo cache aborted.'); + return self::SUCCESS; + } + + $progressBar = new ProgressBar($output, $countFolders); + $progressBar->start(); + + foreach ($folders as $folder) { + try { + $folder->delete(); + } catch (NotPermittedException) { + } + $progressBar->advance(); + } + + $progressBar->finish(); + + $output->writeln(''); + $output->writeln('Contacts photo cache cleared.'); + + return self::SUCCESS; + } +} diff --git a/apps/dav/lib/Command/CreateAddressBook.php b/apps/dav/lib/Command/CreateAddressBook.php index e7d7824d1b6..9626edeba26 100644 --- a/apps/dav/lib/Command/CreateAddressBook.php +++ b/apps/dav/lib/Command/CreateAddressBook.php @@ -24,14 +24,14 @@ class CreateAddressBook extends Command { protected function configure(): void { $this - ->setName('dav:create-addressbook') - ->setDescription('Create a dav addressbook') - ->addArgument('user', - InputArgument::REQUIRED, - 'User for whom the addressbook will be created') - ->addArgument('name', - InputArgument::REQUIRED, - 'Name of the addressbook'); + ->setName('dav:create-addressbook') + ->setDescription('Create a dav addressbook') + ->addArgument('user', + InputArgument::REQUIRED, + 'User for whom the addressbook will be created') + ->addArgument('name', + InputArgument::REQUIRED, + 'Name of the addressbook'); } protected function execute(InputInterface $input, OutputInterface $output): int { diff --git a/apps/dav/lib/Command/CreateCalendar.php b/apps/dav/lib/Command/CreateCalendar.php index 5339d9d1c79..033b5f8d347 100644 --- a/apps/dav/lib/Command/CreateCalendar.php +++ b/apps/dav/lib/Command/CreateCalendar.php @@ -13,11 +13,15 @@ use OCA\DAV\CalDAV\Proxy\ProxyMapper; use OCA\DAV\CalDAV\Sharing\Backend; use OCA\DAV\Connector\Sabre\Principal; use OCP\Accounts\IAccountManager; +use OCP\App\IAppManager; use OCP\EventDispatcher\IEventDispatcher; use OCP\IConfig; use OCP\IDBConnection; use OCP\IGroupManager; use OCP\IUserManager; +use OCP\IUserSession; +use OCP\Security\ISecureRandom; +use OCP\Server; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; @@ -53,19 +57,19 @@ class CreateCalendar extends Command { $principalBackend = new Principal( $this->userManager, $this->groupManager, - \OC::$server->get(IAccountManager::class), - \OC::$server->getShareManager(), - \OC::$server->getUserSession(), - \OC::$server->getAppManager(), - \OC::$server->query(ProxyMapper::class), - \OC::$server->get(KnownUserService::class), - \OC::$server->getConfig(), + Server::get(IAccountManager::class), + Server::get(\OCP\Share\IManager::class), + Server::get(IUserSession::class), + Server::get(IAppManager::class), + Server::get(ProxyMapper::class), + Server::get(KnownUserService::class), + Server::get(IConfig::class), \OC::$server->getL10NFactory(), ); - $random = \OC::$server->getSecureRandom(); - $logger = \OC::$server->get(LoggerInterface::class); - $dispatcher = \OC::$server->get(IEventDispatcher::class); - $config = \OC::$server->get(IConfig::class); + $random = Server::get(ISecureRandom::class); + $logger = Server::get(LoggerInterface::class); + $dispatcher = Server::get(IEventDispatcher::class); + $config = Server::get(IConfig::class); $name = $input->getArgument('name'); $caldav = new CalDavBackend( $this->dbConnection, @@ -75,7 +79,7 @@ class CreateCalendar extends Command { $logger, $dispatcher, $config, - \OC::$server->get(Backend::class), + Server::get(Backend::class), ); $caldav->createCalendar("principals/users/$user", $name, []); return self::SUCCESS; diff --git a/apps/dav/lib/Command/CreateSubscription.php b/apps/dav/lib/Command/CreateSubscription.php new file mode 100644 index 00000000000..1364070e530 --- /dev/null +++ b/apps/dav/lib/Command/CreateSubscription.php @@ -0,0 +1,78 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Command; + +use OCA\DAV\CalDAV\CalDavBackend; +use OCA\Theming\ThemingDefaults; +use OCP\IUserManager; +use Sabre\DAV\Xml\Property\Href; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class CreateSubscription extends Command { + public function __construct( + protected IUserManager $userManager, + private CalDavBackend $caldav, + private ThemingDefaults $themingDefaults, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('dav:create-subscription') + ->setDescription('Create a dav subscription') + ->addArgument('user', + InputArgument::REQUIRED, + 'User for whom the subscription will be created') + ->addArgument('name', + InputArgument::REQUIRED, + 'Name of the subscription to create') + ->addArgument('url', + InputArgument::REQUIRED, + 'Source url of the subscription to create') + ->addArgument('color', + InputArgument::OPTIONAL, + 'Hex color code for the calendar color'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $user = $input->getArgument('user'); + if (!$this->userManager->userExists($user)) { + $output->writeln("<error>User <$user> in unknown.</error>"); + return self::FAILURE; + } + + $name = $input->getArgument('name'); + $url = $input->getArgument('url'); + $color = $input->getArgument('color') ?? $this->themingDefaults->getColorPrimary(); + $subscriptions = $this->caldav->getSubscriptionsForUser("principals/users/$user"); + + $exists = array_filter($subscriptions, function ($row) use ($url) { + return $row['source'] === $url; + }); + + if (!empty($exists)) { + $output->writeln("<error>Subscription for url <$url> already exists for this user.</error>"); + return self::FAILURE; + } + + $urlProperty = new Href($url); + $properties = ['{http://owncloud.org/ns}calendar-enabled' => 1, + '{DAV:}displayname' => $name, + '{http://apple.com/ns/ical/}calendar-color' => $color, + '{http://calendarserver.org/ns/}source' => $urlProperty, + ]; + $this->caldav->createSubscription("principals/users/$user", $name, $properties); + return self::SUCCESS; + } + +} diff --git a/apps/dav/lib/Command/DeleteCalendar.php b/apps/dav/lib/Command/DeleteCalendar.php index 423a9b03434..f6dbed856e6 100644 --- a/apps/dav/lib/Command/DeleteCalendar.php +++ b/apps/dav/lib/Command/DeleteCalendar.php @@ -54,9 +54,9 @@ class DeleteCalendar extends Command { protected function execute( InputInterface $input, - OutputInterface $output + OutputInterface $output, ): int { - /** @var string $user **/ + /** @var string $user */ $user = $input->getArgument('uid'); if (!$this->userManager->userExists($user)) { throw new \InvalidArgumentException( @@ -67,7 +67,7 @@ class DeleteCalendar extends Command { if ($birthday !== false) { $name = BirthdayService::BIRTHDAY_CALENDAR_URI; } else { - /** @var string $name **/ + /** @var string $name */ $name = $input->getArgument('name'); if (!$name) { throw new \InvalidArgumentException( diff --git a/apps/dav/lib/Command/DeleteSubscription.php b/apps/dav/lib/Command/DeleteSubscription.php new file mode 100644 index 00000000000..db0cb6295c9 --- /dev/null +++ b/apps/dav/lib/Command/DeleteSubscription.php @@ -0,0 +1,79 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\Command; + +use OCA\DAV\CalDAV\CachedSubscription; +use OCA\DAV\CalDAV\CalDavBackend; +use OCP\IUserManager; +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +#[AsCommand( + name: 'dav:delete-subscription', + description: 'Delete a calendar subscription for a user', + hidden: false, +)] +class DeleteSubscription extends Command { + public function __construct( + private CalDavBackend $calDavBackend, + private IUserManager $userManager, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this + ->addArgument( + 'uid', + InputArgument::REQUIRED, + 'User who owns the calendar subscription' + ) + ->addArgument( + 'uri', + InputArgument::REQUIRED, + 'URI of the calendar to be deleted' + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $user = (string)$input->getArgument('uid'); + if (!$this->userManager->userExists($user)) { + throw new \InvalidArgumentException("User $user is unknown"); + } + + $uri = (string)$input->getArgument('uri'); + if ($uri === '') { + throw new \InvalidArgumentException('Specify the URI of the calendar to be deleted'); + } + + $subscriptionInfo = $this->calDavBackend->getSubscriptionByUri( + 'principals/users/' . $user, + $uri + ); + + if ($subscriptionInfo === null) { + throw new \InvalidArgumentException("User $user has no calendar subscription with the URI $uri"); + } + + $subscription = new CachedSubscription( + $this->calDavBackend, + $subscriptionInfo, + ); + + $subscription->delete(); + + $output->writeln("Calendar subscription with the URI $uri for user $user deleted"); + + return self::SUCCESS; + } +} diff --git a/apps/dav/lib/Command/ExportCalendar.php b/apps/dav/lib/Command/ExportCalendar.php new file mode 100644 index 00000000000..6ed8aa2cfe4 --- /dev/null +++ b/apps/dav/lib/Command/ExportCalendar.php @@ -0,0 +1,95 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Command; + +use InvalidArgumentException; +use OCA\DAV\CalDAV\Export\ExportService; +use OCP\Calendar\CalendarExportOptions; +use OCP\Calendar\ICalendarExport; +use OCP\Calendar\IManager; +use OCP\IUserManager; +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Calendar Export Command + * + * Used to export data from supported calendars to disk or stdout + */ +#[AsCommand( + name: 'calendar:export', + description: 'Export calendar data from supported calendars to disk or stdout', + hidden: false +)] +class ExportCalendar extends Command { + public function __construct( + private IUserManager $userManager, + private IManager $calendarManager, + private ExportService $exportService, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this->setName('calendar:export') + ->setDescription('Export calendar data from supported calendars to disk or stdout') + ->addArgument('uid', InputArgument::REQUIRED, 'Id of system user') + ->addArgument('uri', InputArgument::REQUIRED, 'Uri of calendar') + ->addOption('format', null, InputOption::VALUE_REQUIRED, 'Format of output (ical, jcal, xcal) defaults to ical', 'ical') + ->addOption('location', null, InputOption::VALUE_REQUIRED, 'Location of where to write the output. defaults to stdout'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $userId = $input->getArgument('uid'); + $calendarId = $input->getArgument('uri'); + $format = $input->getOption('format'); + $location = $input->getOption('location'); + + if (!$this->userManager->userExists($userId)) { + throw new InvalidArgumentException("User <$userId> not found."); + } + // retrieve calendar and evaluate if export is supported + $calendars = $this->calendarManager->getCalendarsForPrincipal('principals/users/' . $userId, [$calendarId]); + if ($calendars === []) { + throw new InvalidArgumentException("Calendar <$calendarId> not found."); + } + $calendar = $calendars[0]; + if (!$calendar instanceof ICalendarExport) { + throw new InvalidArgumentException("Calendar <$calendarId> does not support exporting"); + } + // construct options object + $options = new CalendarExportOptions(); + // evaluate if provided format is supported + if (!in_array($format, ExportService::FORMATS, true)) { + throw new InvalidArgumentException("Format <$format> is not valid."); + } + $options->setFormat($format); + // evaluate is a valid location was given and is usable otherwise output to stdout + if ($location !== null) { + $handle = fopen($location, 'wb'); + if ($handle === false) { + throw new InvalidArgumentException("Location <$location> is not valid. Can not open location for write operation."); + } + + foreach ($this->exportService->export($calendar, $options) as $chunk) { + fwrite($handle, $chunk); + } + fclose($handle); + } else { + foreach ($this->exportService->export($calendar, $options) as $chunk) { + $output->writeln($chunk); + } + } + + return self::SUCCESS; + } +} diff --git a/apps/dav/lib/Command/FixCalendarSyncCommand.php b/apps/dav/lib/Command/FixCalendarSyncCommand.php index 5e92b3270d2..cb31355c10d 100644 --- a/apps/dav/lib/Command/FixCalendarSyncCommand.php +++ b/apps/dav/lib/Command/FixCalendarSyncCommand.php @@ -20,8 +20,10 @@ use Symfony\Component\Console\Output\OutputInterface; class FixCalendarSyncCommand extends Command { - public function __construct(private IUserManager $userManager, - private CalDavBackend $calDavBackend) { + public function __construct( + private IUserManager $userManager, + private CalDavBackend $calDavBackend, + ) { parent::__construct('dav:fix-missing-caldav-changes'); } @@ -41,22 +43,23 @@ class FixCalendarSyncCommand extends Command { $user = $this->userManager->get($userArg); if ($user === null) { $output->writeln("<error>User $userArg does not exist</error>"); - return 1; + return self::FAILURE; } $this->fixUserCalendars($user); } else { $progress = new ProgressBar($output); - $this->userManager->callForSeenUsers(function (IUser $user) use ($progress) { + $this->userManager->callForSeenUsers(function (IUser $user) use ($progress): void { $this->fixUserCalendars($user, $progress); }); $progress->finish(); } - return 0; + $output->writeln(''); + return self::SUCCESS; } private function fixUserCalendars(IUser $user, ?ProgressBar $progress = null): void { - $calendars = $this->calDavBackend->getCalendarsForUser("principals/users/" . $user->getUID()); + $calendars = $this->calDavBackend->getCalendarsForUser('principals/users/' . $user->getUID()); foreach ($calendars as $calendar) { $this->calDavBackend->restoreChanges($calendar['id']); diff --git a/apps/dav/lib/Command/GetAbsenceCommand.php b/apps/dav/lib/Command/GetAbsenceCommand.php new file mode 100644 index 00000000000..50d8df4ab38 --- /dev/null +++ b/apps/dav/lib/Command/GetAbsenceCommand.php @@ -0,0 +1,62 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +namespace OCA\DAV\Command; + +use OCA\DAV\Service\AbsenceService; +use OCP\IUserManager; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class GetAbsenceCommand extends Command { + + public function __construct( + private IUserManager $userManager, + private AbsenceService $absenceService, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this->setName('dav:absence:get'); + $this->addArgument( + 'user-id', + InputArgument::REQUIRED, + 'User ID of the affected account' + ); + } + + public function execute(InputInterface $input, OutputInterface $output): int { + $userId = $input->getArgument('user-id'); + + $user = $this->userManager->get($userId); + if ($user === null) { + $output->writeln('<error>User not found</error>'); + return 1; + } + + $absence = $this->absenceService->getAbsence($userId); + if ($absence === null) { + $output->writeln('<info>No absence set</info>'); + return 0; + } + + $output->writeln('<info>Start day:</info> ' . $absence->getFirstDay()); + $output->writeln('<info>End day:</info> ' . $absence->getLastDay()); + $output->writeln('<info>Short message:</info> ' . $absence->getStatus()); + $output->writeln('<info>Message:</info> ' . $absence->getMessage()); + $output->writeln('<info>Replacement user:</info> ' . ($absence->getReplacementUserId() ?? 'none')); + $output->writeln('<info>Replacement display name:</info> ' . ($absence->getReplacementUserDisplayName() ?? 'none')); + + return 0; + } + +} diff --git a/apps/dav/lib/Command/ListAddressbooks.php b/apps/dav/lib/Command/ListAddressbooks.php new file mode 100644 index 00000000000..c0b6e63ccb8 --- /dev/null +++ b/apps/dav/lib/Command/ListAddressbooks.php @@ -0,0 +1,76 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Command; + +use OCA\DAV\CardDAV\CardDavBackend; +use OCA\DAV\CardDAV\SystemAddressbook; +use OCP\IUserManager; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\Table; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class ListAddressbooks extends Command { + public function __construct( + protected IUserManager $userManager, + private CardDavBackend $cardDavBackend, + ) { + parent::__construct('dav:list-addressbooks'); + } + + protected function configure(): void { + $this + ->setDescription('List all addressbooks of a user') + ->addArgument('uid', + InputArgument::REQUIRED, + 'User for whom all addressbooks will be listed'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $user = $input->getArgument('uid'); + if (!$this->userManager->userExists($user)) { + throw new \InvalidArgumentException("User <$user> is unknown."); + } + + $addressBooks = $this->cardDavBackend->getAddressBooksForUser("principals/users/$user"); + + $addressBookTableData = []; + foreach ($addressBooks as $book) { + // skip system / contacts integration address book + if ($book['uri'] === SystemAddressbook::URI_SHARED) { + continue; + } + + $readOnly = false; + $readOnlyIndex = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only'; + if (isset($book[$readOnlyIndex])) { + $readOnly = $book[$readOnlyIndex]; + } + + $addressBookTableData[] = [ + $book['uri'], + $book['{DAV:}displayname'], + $book['{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal'] ?? $book['principaluri'], + $book['{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname'], + $readOnly ? ' x ' : ' ✓ ', + ]; + } + + if (count($addressBookTableData) > 0) { + $table = new Table($output); + $table->setHeaders(['Database ID', 'URI', 'Displayname', 'Owner principal', 'Owner displayname', 'Writable']) + ->setRows($addressBookTableData); + + $table->render(); + } else { + $output->writeln("<info>User <$user> has no addressbooks</info>"); + } + return self::SUCCESS; + } +} diff --git a/apps/dav/lib/Command/ListCalendarShares.php b/apps/dav/lib/Command/ListCalendarShares.php new file mode 100644 index 00000000000..2729bc80530 --- /dev/null +++ b/apps/dav/lib/Command/ListCalendarShares.php @@ -0,0 +1,131 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\Command; + +use OCA\DAV\CalDAV\CalDavBackend; +use OCA\DAV\CalDAV\Sharing\Backend; +use OCA\DAV\Connector\Sabre\Principal; +use OCA\DAV\DAV\Sharing\SharingMapper; +use OCP\IUserManager; +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\Table; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +#[AsCommand( + name: 'dav:list-calendar-shares', + description: 'List all calendar shares for a user', + hidden: false, +)] +class ListCalendarShares extends Command { + public function __construct( + private IUserManager $userManager, + private Principal $principal, + private CalDavBackend $caldav, + private SharingMapper $mapper, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this->addArgument( + 'uid', + InputArgument::REQUIRED, + 'User whose calendar shares will be listed' + ); + $this->addOption( + 'calendar-id', + '', + InputOption::VALUE_REQUIRED, + 'List only shares for the given calendar id id', + null, + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $user = (string)$input->getArgument('uid'); + if (!$this->userManager->userExists($user)) { + throw new \InvalidArgumentException("User $user is unknown"); + } + + $principal = $this->principal->getPrincipalByPath('principals/users/' . $user); + if ($principal === null) { + throw new \InvalidArgumentException("Unable to fetch principal for user $user"); + } + + $memberships = array_merge( + [$principal['uri']], + $this->principal->getGroupMembership($principal['uri']), + $this->principal->getCircleMembership($principal['uri']), + ); + + $shares = $this->mapper->getSharesByPrincipals($memberships, 'calendar'); + + $calendarId = $input->getOption('calendar-id'); + if ($calendarId !== null) { + $shares = array_filter($shares, fn ($share) => $share['resourceid'] === (int)$calendarId); + } + + $rows = array_map(fn ($share) => $this->formatCalendarShare($share), $shares); + + if (count($rows) > 0) { + $table = new Table($output); + $table + ->setHeaders(['Share Id', 'Calendar Id', 'Calendar URI', 'Calendar Name', 'Calendar Owner', 'Access By', 'Permissions']) + ->setRows($rows) + ->render(); + } else { + $output->writeln("User $user has no calendar shares"); + } + + return self::SUCCESS; + } + + private function formatCalendarShare(array $share): array { + $calendarInfo = $this->caldav->getCalendarById($share['resourceid']); + + $calendarUri = 'Resource not found'; + $calendarName = ''; + $calendarOwner = ''; + + if ($calendarInfo !== null) { + $calendarUri = $calendarInfo['uri']; + $calendarName = $calendarInfo['{DAV:}displayname']; + $calendarOwner = $calendarInfo['{http://nextcloud.com/ns}owner-displayname'] . ' (' . $calendarInfo['principaluri'] . ')'; + } + + $accessBy = match (true) { + str_starts_with($share['principaluri'], 'principals/users/') => 'Individual', + str_starts_with($share['principaluri'], 'principals/groups/') => 'Group (' . $share['principaluri'] . ')', + str_starts_with($share['principaluri'], 'principals/circles/') => 'Team (' . $share['principaluri'] . ')', + default => $share['principaluri'], + }; + + $permissions = match ($share['access']) { + Backend::ACCESS_READ => 'Read', + Backend::ACCESS_READ_WRITE => 'Read/Write', + Backend::ACCESS_UNSHARED => 'Unshare', + default => $share['access'], + }; + + return [ + $share['id'], + $share['resourceid'], + $calendarUri, + $calendarName, + $calendarOwner, + $accessBy, + $permissions, + ]; + } +} diff --git a/apps/dav/lib/Command/ListCalendars.php b/apps/dav/lib/Command/ListCalendars.php index 5344530e8a5..408a7e5247f 100644 --- a/apps/dav/lib/Command/ListCalendars.php +++ b/apps/dav/lib/Command/ListCalendars.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -63,7 +64,7 @@ class ListCalendars extends Command { if (count($calendarTableData) > 0) { $table = new Table($output); - $table->setHeaders(['uri', 'displayname', 'owner\'s userid', 'owner\'s displayname', 'writable']) + $table->setHeaders(['URI', 'Displayname', 'Owner principal', 'Owner displayname', 'Writable']) ->setRows($calendarTableData); $table->render(); diff --git a/apps/dav/lib/Command/ListSubscriptions.php b/apps/dav/lib/Command/ListSubscriptions.php new file mode 100644 index 00000000000..67753f25973 --- /dev/null +++ b/apps/dav/lib/Command/ListSubscriptions.php @@ -0,0 +1,77 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\Command; + +use OCA\DAV\CalDAV\CalDavBackend; +use OCP\IAppConfig; +use OCP\IUserManager; +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\Table; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +#[AsCommand( + name: 'dav:list-subscriptions', + description: 'List all calendar subscriptions for a user', + hidden: false, +)] +class ListSubscriptions extends Command { + public function __construct( + private IUserManager $userManager, + private IAppConfig $appConfig, + private CalDavBackend $caldav, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this->addArgument( + 'uid', + InputArgument::REQUIRED, + 'User whose calendar subscriptions will be listed' + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $user = (string)$input->getArgument('uid'); + if (!$this->userManager->userExists($user)) { + throw new \InvalidArgumentException("User $user is unknown"); + } + + $defaultRefreshRate = $this->appConfig->getValueString('dav', 'calendarSubscriptionRefreshRate', 'P1D'); + $subscriptions = $this->caldav->getSubscriptionsForUser("principals/users/$user"); + $rows = []; + + foreach ($subscriptions as $subscription) { + $rows[] = [ + $subscription['uri'], + $subscription['{DAV:}displayname'], + $subscription['{http://apple.com/ns/ical/}refreshrate'] ?? ($defaultRefreshRate . ' (default)'), + $subscription['source'], + ]; + } + + usort($rows, static fn (array $a, array $b) => $a[0] <=> $b[0]); + + if (count($rows) > 0) { + $table = new Table($output); + $table + ->setHeaders(['URI', 'Displayname', 'Refresh rate', 'Source']) + ->setRows($rows) + ->render(); + } else { + $output->writeln("User $user has no subscriptions"); + } + + return self::SUCCESS; + } +} diff --git a/apps/dav/lib/Command/MoveCalendar.php b/apps/dav/lib/Command/MoveCalendar.php index b8adc75338a..b8acc191cc3 100644 --- a/apps/dav/lib/Command/MoveCalendar.php +++ b/apps/dav/lib/Command/MoveCalendar.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -50,7 +51,7 @@ class MoveCalendar extends Command { ->addArgument('destinationuid', InputArgument::REQUIRED, 'User who will receive the calendar') - ->addOption('force', 'f', InputOption::VALUE_NONE, "Force the migration by removing existing shares and renaming calendars in case of conflicts"); + ->addOption('force', 'f', InputOption::VALUE_NONE, 'Force the migration by removing existing shares and renaming calendars in case of conflicts'); } protected function execute(InputInterface $input, OutputInterface $output): int { @@ -97,8 +98,8 @@ class MoveCalendar extends Command { * Warn that share links have changed if there are shares */ $this->io->note([ - "Please note that moving calendar " . $calendar['uri'] . " from user <$userOrigin> to <$userDestination> has caused share links to change.", - "Sharees will need to change \"example.com/remote.php/dav/calendars/uid/" . $calendar['uri'] . "_shared_by_$userOrigin\" to \"example.com/remote.php/dav/calendars/uid/" . $newName ?: $calendar['uri'] . "_shared_by_$userDestination\"" + 'Please note that moving calendar ' . $calendar['uri'] . " from user <$userOrigin> to <$userDestination> has caused share links to change.", + 'Sharees will need to change "example.com/remote.php/dav/calendars/uid/' . $calendar['uri'] . "_shared_by_$userOrigin\" to \"example.com/remote.php/dav/calendars/uid/" . $newName ?: $calendar['uri'] . "_shared_by_$userDestination\"" ]); } @@ -155,7 +156,7 @@ class MoveCalendar extends Command { if ($force) { $this->calDav->updateShares(new Calendar($this->calDav, $calendar, $this->l10n, $this->config, $this->logger), [], ['principal:principals/groups/' . $userOrGroup]); } else { - throw new \InvalidArgumentException("User <$userDestination> is not part of the group <$userOrGroup> with whom the calendar <" . $calendar['uri'] . "> was shared. You may use -f to move the calendar while deleting this share."); + throw new \InvalidArgumentException("User <$userDestination> is not part of the group <$userOrGroup> with whom the calendar <" . $calendar['uri'] . '> was shared. You may use -f to move the calendar while deleting this share.'); } } @@ -166,7 +167,7 @@ class MoveCalendar extends Command { if ($force) { $this->calDav->updateShares(new Calendar($this->calDav, $calendar, $this->l10n, $this->config, $this->logger), [], ['principal:principals/users/' . $userOrGroup]); } else { - throw new \InvalidArgumentException("The calendar <" . $calendar['uri'] . "> is already shared to user <$userDestination>.You may use -f to move the calendar while deleting this share."); + throw new \InvalidArgumentException('The calendar <' . $calendar['uri'] . "> is already shared to user <$userDestination>.You may use -f to move the calendar while deleting this share."); } } } diff --git a/apps/dav/lib/Command/RemoveInvalidShares.php b/apps/dav/lib/Command/RemoveInvalidShares.php index 18f8de5b4e2..340e878a912 100644 --- a/apps/dav/lib/Command/RemoveInvalidShares.php +++ b/apps/dav/lib/Command/RemoveInvalidShares.php @@ -38,7 +38,7 @@ class RemoveInvalidShares extends Command { $query = $this->connection->getQueryBuilder(); $result = $query->selectDistinct('principaluri') ->from('dav_shares') - ->execute(); + ->executeQuery(); while ($row = $result->fetch()) { $principaluri = $row['principaluri']; @@ -59,6 +59,6 @@ class RemoveInvalidShares extends Command { $delete = $this->connection->getQueryBuilder(); $delete->delete('dav_shares') ->where($delete->expr()->eq('principaluri', $delete->createNamedParameter($principaluri))); - $delete->execute(); + $delete->executeStatement(); } } diff --git a/apps/dav/lib/Command/SendEventReminders.php b/apps/dav/lib/Command/SendEventReminders.php index f5afb30ed70..89bb5ce8c20 100644 --- a/apps/dav/lib/Command/SendEventReminders.php +++ b/apps/dav/lib/Command/SendEventReminders.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/lib/Command/SetAbsenceCommand.php b/apps/dav/lib/Command/SetAbsenceCommand.php new file mode 100644 index 00000000000..bf91a163f95 --- /dev/null +++ b/apps/dav/lib/Command/SetAbsenceCommand.php @@ -0,0 +1,95 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +namespace OCA\DAV\Command; + +use OCA\DAV\Service\AbsenceService; +use OCP\IUserManager; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class SetAbsenceCommand extends Command { + + public function __construct( + private IUserManager $userManager, + private AbsenceService $absenceService, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this->setName('dav:absence:set'); + $this->addArgument( + 'user-id', + InputArgument::REQUIRED, + 'User ID of the affected account' + ); + $this->addArgument( + 'first-day', + InputArgument::REQUIRED, + 'Inclusive start day formatted as YYYY-MM-DD' + ); + $this->addArgument( + 'last-day', + InputArgument::REQUIRED, + 'Inclusive end day formatted as YYYY-MM-DD' + ); + $this->addArgument( + 'short-message', + InputArgument::REQUIRED, + 'Short message' + ); + $this->addArgument( + 'message', + InputArgument::REQUIRED, + 'Message' + ); + $this->addArgument( + 'replacement-user-id', + InputArgument::OPTIONAL, + 'Replacement user id' + ); + } + + public function execute(InputInterface $input, OutputInterface $output): int { + $userId = $input->getArgument('user-id'); + + $user = $this->userManager->get($userId); + if ($user === null) { + $output->writeln('<error>User not found</error>'); + return 1; + } + + $replacementUserId = $input->getArgument('replacement-user-id'); + if ($replacementUserId === null) { + $replacementUser = null; + } else { + $replacementUser = $this->userManager->get($replacementUserId); + if ($replacementUser === null) { + $output->writeln('<error>Replacement user not found</error>'); + return 2; + } + } + + $this->absenceService->createOrUpdateAbsence( + $user, + $input->getArgument('first-day'), + $input->getArgument('last-day'), + $input->getArgument('short-message'), + $input->getArgument('message'), + $replacementUser?->getUID(), + $replacementUser?->getDisplayName(), + ); + + return 0; + } + +} diff --git a/apps/dav/lib/Command/SyncBirthdayCalendar.php b/apps/dav/lib/Command/SyncBirthdayCalendar.php index 5c8ac913af4..db1ebb6ecb5 100644 --- a/apps/dav/lib/Command/SyncBirthdayCalendar.php +++ b/apps/dav/lib/Command/SyncBirthdayCalendar.php @@ -55,10 +55,10 @@ class SyncBirthdayCalendar extends Command { $this->birthdayService->syncUser($user); return self::SUCCESS; } - $output->writeln("Start birthday calendar sync for all users ..."); + $output->writeln('Start birthday calendar sync for all users ...'); $p = new ProgressBar($output); $p->start(); - $this->userManager->callForSeenUsers(function ($user) use ($p) { + $this->userManager->callForSeenUsers(function ($user) use ($p): void { $p->advance(); $userId = $user->getUID(); diff --git a/apps/dav/lib/Command/SyncSystemAddressBook.php b/apps/dav/lib/Command/SyncSystemAddressBook.php index 715d3d65f87..54edba01e05 100644 --- a/apps/dav/lib/Command/SyncSystemAddressBook.php +++ b/apps/dav/lib/Command/SyncSystemAddressBook.php @@ -32,7 +32,7 @@ class SyncSystemAddressBook extends Command { $output->writeln('Syncing users ...'); $progress = new ProgressBar($output); $progress->start(); - $this->syncService->syncInstance(function () use ($progress) { + $this->syncService->syncInstance(function () use ($progress): void { $progress->advance(); }); |