diff options
Diffstat (limited to 'apps/dav/lib/Command')
21 files changed, 1720 insertions, 0 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 new file mode 100644 index 00000000000..9626edeba26 --- /dev/null +++ b/apps/dav/lib/Command/CreateAddressBook.php @@ -0,0 +1,47 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Command; + +use OCA\DAV\CardDAV\CardDavBackend; +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 CreateAddressBook extends Command { + public function __construct( + private IUserManager $userManager, + private CardDavBackend $cardDavBackend, + ) { + parent::__construct(); + } + + 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'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $user = $input->getArgument('user'); + if (!$this->userManager->userExists($user)) { + throw new \InvalidArgumentException("User <$user> in unknown."); + } + + $name = $input->getArgument('name'); + $this->cardDavBackend->createAddressBook("principals/users/$user", $name, []); + return self::SUCCESS; + } +} diff --git a/apps/dav/lib/Command/CreateCalendar.php b/apps/dav/lib/Command/CreateCalendar.php new file mode 100644 index 00000000000..033b5f8d347 --- /dev/null +++ b/apps/dav/lib/Command/CreateCalendar.php @@ -0,0 +1,87 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Command; + +use OC\KnownUser\KnownUserService; +use OCA\DAV\CalDAV\CalDavBackend; +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; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class CreateCalendar extends Command { + public function __construct( + protected IUserManager $userManager, + private IGroupManager $groupManager, + protected IDBConnection $dbConnection, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('dav:create-calendar') + ->setDescription('Create a dav calendar') + ->addArgument('user', + InputArgument::REQUIRED, + 'User for whom the calendar will be created') + ->addArgument('name', + InputArgument::REQUIRED, + 'Name of the calendar'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $user = $input->getArgument('user'); + if (!$this->userManager->userExists($user)) { + throw new \InvalidArgumentException("User <$user> in unknown."); + } + $principalBackend = new Principal( + $this->userManager, + $this->groupManager, + 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 = 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, + $principalBackend, + $this->userManager, + $random, + $logger, + $dispatcher, + $config, + 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 new file mode 100644 index 00000000000..f6dbed856e6 --- /dev/null +++ b/apps/dav/lib/Command/DeleteCalendar.php @@ -0,0 +1,103 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\Command; + +use OCA\DAV\CalDAV\BirthdayService; +use OCA\DAV\CalDAV\CalDavBackend; +use OCA\DAV\CalDAV\Calendar; +use OCP\IConfig; +use OCP\IL10N; +use OCP\IUserManager; +use Psr\Log\LoggerInterface; +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; + +class DeleteCalendar extends Command { + public function __construct( + private CalDavBackend $calDav, + private IConfig $config, + private IL10N $l10n, + private IUserManager $userManager, + private LoggerInterface $logger, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('dav:delete-calendar') + ->setDescription('Delete a dav calendar') + ->addArgument('uid', + InputArgument::REQUIRED, + 'User who owns the calendar') + ->addArgument('name', + InputArgument::OPTIONAL, + 'Name of the calendar to delete') + ->addOption('birthday', + null, + InputOption::VALUE_NONE, + 'Delete the birthday calendar') + ->addOption('force', + 'f', + InputOption::VALUE_NONE, + 'Force delete skipping trashbin'); + } + + protected function execute( + InputInterface $input, + OutputInterface $output, + ): int { + /** @var string $user */ + $user = $input->getArgument('uid'); + if (!$this->userManager->userExists($user)) { + throw new \InvalidArgumentException( + 'User <' . $user . '> is unknown.'); + } + + $birthday = $input->getOption('birthday'); + if ($birthday !== false) { + $name = BirthdayService::BIRTHDAY_CALENDAR_URI; + } else { + /** @var string $name */ + $name = $input->getArgument('name'); + if (!$name) { + throw new \InvalidArgumentException( + 'Please specify a calendar name or --birthday'); + } + } + + $calendarInfo = $this->calDav->getCalendarByUri( + 'principals/users/' . $user, + $name); + if ($calendarInfo === null) { + throw new \InvalidArgumentException( + 'User <' . $user . '> has no calendar named <' . $name . '>. You can run occ dav:list-calendars to list calendars URIs for this user.'); + } + + $calendar = new Calendar( + $this->calDav, + $calendarInfo, + $this->l10n, + $this->config, + $this->logger + ); + + $force = $input->getOption('force'); + if ($force) { + $calendar->disableTrashbin(); + } + + $calendar->delete(); + + return self::SUCCESS; + } +} 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 new file mode 100644 index 00000000000..cb31355c10d --- /dev/null +++ b/apps/dav/lib/Command/FixCalendarSyncCommand.php @@ -0,0 +1,73 @@ +<?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\CalDAV\CalDavBackend; +use OCP\IUser; +use OCP\IUserManager; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\ProgressBar; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class FixCalendarSyncCommand extends Command { + + public function __construct( + private IUserManager $userManager, + private CalDavBackend $calDavBackend, + ) { + parent::__construct('dav:fix-missing-caldav-changes'); + } + + protected function configure(): void { + $this->setDescription('Insert missing calendarchanges rows for existing events'); + $this->addArgument( + 'user', + InputArgument::OPTIONAL, + 'User to fix calendar sync tokens for, if omitted all users will be fixed', + null, + ); + } + + public function execute(InputInterface $input, OutputInterface $output): int { + $userArg = $input->getArgument('user'); + if ($userArg !== null) { + $user = $this->userManager->get($userArg); + if ($user === null) { + $output->writeln("<error>User $userArg does not exist</error>"); + return self::FAILURE; + } + + $this->fixUserCalendars($user); + } else { + $progress = new ProgressBar($output); + $this->userManager->callForSeenUsers(function (IUser $user) use ($progress): void { + $this->fixUserCalendars($user, $progress); + }); + $progress->finish(); + } + $output->writeln(''); + return self::SUCCESS; + } + + private function fixUserCalendars(IUser $user, ?ProgressBar $progress = null): void { + $calendars = $this->calDavBackend->getCalendarsForUser('principals/users/' . $user->getUID()); + + foreach ($calendars as $calendar) { + $this->calDavBackend->restoreChanges($calendar['id']); + } + + if ($progress !== null) { + $progress->advance(); + } + } + +} 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 new file mode 100644 index 00000000000..408a7e5247f --- /dev/null +++ b/apps/dav/lib/Command/ListCalendars.php @@ -0,0 +1,76 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Command; + +use OCA\DAV\CalDAV\BirthdayService; +use OCA\DAV\CalDAV\CalDavBackend; +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 ListCalendars extends Command { + public function __construct( + protected IUserManager $userManager, + private CalDavBackend $caldav, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('dav:list-calendars') + ->setDescription('List all calendars of a user') + ->addArgument('uid', + InputArgument::REQUIRED, + 'User for whom all calendars 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."); + } + + $calendars = $this->caldav->getCalendarsForUser("principals/users/$user"); + + $calendarTableData = []; + foreach ($calendars as $calendar) { + // skip birthday calendar + if ($calendar['uri'] === BirthdayService::BIRTHDAY_CALENDAR_URI) { + continue; + } + + $readOnly = false; + $readOnlyIndex = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only'; + if (isset($calendar[$readOnlyIndex])) { + $readOnly = $calendar[$readOnlyIndex]; + } + + $calendarTableData[] = [ + $calendar['uri'], + $calendar['{DAV:}displayname'], + $calendar['{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal'], + $calendar['{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname'], + $readOnly ? ' x ' : ' ✓ ', + ]; + } + + if (count($calendarTableData) > 0) { + $table = new Table($output); + $table->setHeaders(['URI', 'Displayname', 'Owner principal', 'Owner displayname', 'Writable']) + ->setRows($calendarTableData); + + $table->render(); + } else { + $output->writeln("<info>User <$user> has no calendars</info>"); + } + return self::SUCCESS; + } +} 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 new file mode 100644 index 00000000000..b8acc191cc3 --- /dev/null +++ b/apps/dav/lib/Command/MoveCalendar.php @@ -0,0 +1,177 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016 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\Calendar; +use OCP\IConfig; +use OCP\IGroupManager; +use OCP\IL10N; +use OCP\IUserManager; +use OCP\Share\IManager as IShareManager; +use Psr\Log\LoggerInterface; +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; +use Symfony\Component\Console\Style\SymfonyStyle; + +class MoveCalendar extends Command { + private ?SymfonyStyle $io = null; + + public const URI_USERS = 'principals/users/'; + + public function __construct( + private IUserManager $userManager, + private IGroupManager $groupManager, + private IShareManager $shareManager, + private IConfig $config, + private IL10N $l10n, + private CalDavBackend $calDav, + private LoggerInterface $logger, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('dav:move-calendar') + ->setDescription('Move a calendar from an user to another') + ->addArgument('name', + InputArgument::REQUIRED, + 'Name of the calendar to move') + ->addArgument('sourceuid', + InputArgument::REQUIRED, + 'User who currently owns the calendar') + ->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'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $userOrigin = $input->getArgument('sourceuid'); + $userDestination = $input->getArgument('destinationuid'); + + $this->io = new SymfonyStyle($input, $output); + + if (!$this->userManager->userExists($userOrigin)) { + throw new \InvalidArgumentException("User <$userOrigin> is unknown."); + } + + if (!$this->userManager->userExists($userDestination)) { + throw new \InvalidArgumentException("User <$userDestination> is unknown."); + } + + $name = $input->getArgument('name'); + $newName = null; + + $calendar = $this->calDav->getCalendarByUri(self::URI_USERS . $userOrigin, $name); + + if ($calendar === null) { + throw new \InvalidArgumentException("User <$userOrigin> has no calendar named <$name>. You can run occ dav:list-calendars to list calendars URIs for this user."); + } + + // Calendar already exists + if ($this->calendarExists($userDestination, $name)) { + if ($input->getOption('force')) { + // Try to find a suitable name + $newName = $this->getNewCalendarName($userDestination, $name); + + // If we didn't find a suitable value after all the iterations, give up + if ($this->calendarExists($userDestination, $newName)) { + throw new \InvalidArgumentException("Unable to find a suitable calendar name for <$userDestination> with initial name <$name>."); + } + } else { + throw new \InvalidArgumentException("User <$userDestination> already has a calendar named <$name>."); + } + } + + $hadShares = $this->checkShares($calendar, $userOrigin, $userDestination, $input->getOption('force')); + if ($hadShares) { + /** + * 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\"" + ]); + } + + $this->calDav->moveCalendar($name, self::URI_USERS . $userOrigin, self::URI_USERS . $userDestination, $newName); + + $this->io->success("Calendar <$name> was moved from user <$userOrigin> to <$userDestination>" . ($newName ? " as <$newName>" : '')); + return self::SUCCESS; + } + + /** + * Check if the calendar exists for user + */ + protected function calendarExists(string $userDestination, string $name): bool { + return $this->calDav->getCalendarByUri(self::URI_USERS . $userDestination, $name) !== null; + } + + /** + * Try to find a suitable new calendar name that + * doesn't exist for the provided user + */ + protected function getNewCalendarName(string $userDestination, string $name): string { + $increment = 1; + $newName = $name . '-' . $increment; + while ($increment <= 10) { + $this->io->writeln("Trying calendar name <$newName>", OutputInterface::VERBOSITY_VERBOSE); + if (!$this->calendarExists($userDestination, $newName)) { + // New name is good to go + $this->io->writeln("Found proper new calendar name <$newName>", OutputInterface::VERBOSITY_VERBOSE); + break; + } + $newName = $name . '-' . $increment; + $increment++; + } + + return $newName; + } + + /** + * Check that moving the calendar won't break shares + * + * @return bool had any shares or not + * @throws \InvalidArgumentException + */ + private function checkShares(array $calendar, string $userOrigin, string $userDestination, bool $force = false): bool { + $shares = $this->calDav->getShares($calendar['id']); + foreach ($shares as $share) { + [, $prefix, $userOrGroup] = explode('/', $share['href'], 3); + + /** + * Check that user destination is member of the groups which whom the calendar was shared + * If we ask to force the migration, the share with the group is dropped + */ + if ($this->shareManager->shareWithGroupMembersOnly() === true && $prefix === 'groups' && !$this->groupManager->isInGroup($userDestination, $userOrGroup)) { + 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.'); + } + } + + /** + * Check that calendar isn't already shared with user destination + */ + if ($userOrGroup === $userDestination) { + 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."); + } + } + } + + return count($shares) > 0; + } +} diff --git a/apps/dav/lib/Command/RemoveInvalidShares.php b/apps/dav/lib/Command/RemoveInvalidShares.php new file mode 100644 index 00000000000..340e878a912 --- /dev/null +++ b/apps/dav/lib/Command/RemoveInvalidShares.php @@ -0,0 +1,64 @@ +<?php + +declare(strict_types=1); + + +/** + * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2018 ownCloud GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Command; + +use OCA\DAV\Connector\Sabre\Principal; +use OCP\IDBConnection; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Class RemoveInvalidShares - removes shared calendars and addressbook which + * have no matching principal. Happened because of a bug in the calendar app. + */ +class RemoveInvalidShares extends Command { + public function __construct( + private IDBConnection $connection, + private Principal $principalBackend, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('dav:remove-invalid-shares') + ->setDescription('Remove invalid dav shares'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $query = $this->connection->getQueryBuilder(); + $result = $query->selectDistinct('principaluri') + ->from('dav_shares') + ->executeQuery(); + + while ($row = $result->fetch()) { + $principaluri = $row['principaluri']; + $p = $this->principalBackend->getPrincipalByPath($principaluri); + if ($p === null) { + $this->deleteSharesForPrincipal($principaluri); + } + } + + $result->closeCursor(); + return self::SUCCESS; + } + + /** + * @param string $principaluri + */ + private function deleteSharesForPrincipal($principaluri): void { + $delete = $this->connection->getQueryBuilder(); + $delete->delete('dav_shares') + ->where($delete->expr()->eq('principaluri', $delete->createNamedParameter($principaluri))); + $delete->executeStatement(); + } +} diff --git a/apps/dav/lib/Command/RetentionCleanupCommand.php b/apps/dav/lib/Command/RetentionCleanupCommand.php new file mode 100644 index 00000000000..f1c941af20e --- /dev/null +++ b/apps/dav/lib/Command/RetentionCleanupCommand.php @@ -0,0 +1,28 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Command; + +use OCA\DAV\CalDAV\RetentionService; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class RetentionCleanupCommand extends Command { + public function __construct( + private RetentionService $service, + ) { + parent::__construct('dav:retention:clean-up'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $this->service->cleanUp(); + + return self::SUCCESS; + } +} diff --git a/apps/dav/lib/Command/SendEventReminders.php b/apps/dav/lib/Command/SendEventReminders.php new file mode 100644 index 00000000000..89bb5ce8c20 --- /dev/null +++ b/apps/dav/lib/Command/SendEventReminders.php @@ -0,0 +1,53 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Command; + +use OCA\DAV\CalDAV\Reminder\ReminderService; +use OCP\IConfig; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Class SendEventReminders + * + * @package OCA\DAV\Command + */ +class SendEventReminders extends Command { + public function __construct( + protected ReminderService $reminderService, + protected IConfig $config, + ) { + parent::__construct(); + } + + /** + * @inheritDoc + */ + protected function configure():void { + $this + ->setName('dav:send-event-reminders') + ->setDescription('Sends event reminders'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + if ($this->config->getAppValue('dav', 'sendEventReminders', 'yes') !== 'yes') { + $output->writeln('<error>Sending event reminders disabled!</error>'); + $output->writeln('<info>Please run "php occ config:app:set dav sendEventReminders --value yes"'); + return self::FAILURE; + } + + if ($this->config->getAppValue('dav', 'sendEventRemindersMode', 'backgroundjob') !== 'occ') { + $output->writeln('<error>Sending event reminders mode set to background-job!</error>'); + $output->writeln('<info>Please run "php occ config:app:set dav sendEventRemindersMode --value occ"'); + return self::FAILURE; + } + + $this->reminderService->processReminders(); + return self::SUCCESS; + } +} 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 new file mode 100644 index 00000000000..db1ebb6ecb5 --- /dev/null +++ b/apps/dav/lib/Command/SyncBirthdayCalendar.php @@ -0,0 +1,86 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Command; + +use OCA\DAV\CalDAV\BirthdayService; +use OCP\IConfig; +use OCP\IUser; +use OCP\IUserManager; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\ProgressBar; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class SyncBirthdayCalendar extends Command { + public function __construct( + private IUserManager $userManager, + private IConfig $config, + private BirthdayService $birthdayService, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('dav:sync-birthday-calendar') + ->setDescription('Synchronizes the birthday calendar') + ->addArgument('user', + InputArgument::OPTIONAL, + 'User for whom the birthday calendar will be synchronized'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $this->verifyEnabled(); + + $user = $input->getArgument('user'); + if (!is_null($user)) { + if (!$this->userManager->userExists($user)) { + throw new \InvalidArgumentException("User <$user> in unknown."); + } + + // re-enable the birthday calendar in case it's called directly with a user name + $isEnabled = $this->config->getUserValue($user, 'dav', 'generateBirthdayCalendar', 'yes'); + if ($isEnabled !== 'yes') { + $this->config->setUserValue($user, 'dav', 'generateBirthdayCalendar', 'yes'); + $output->writeln("Re-enabling birthday calendar for $user"); + } + + $output->writeln("Start birthday calendar sync for $user"); + $this->birthdayService->syncUser($user); + return self::SUCCESS; + } + $output->writeln('Start birthday calendar sync for all users ...'); + $p = new ProgressBar($output); + $p->start(); + $this->userManager->callForSeenUsers(function ($user) use ($p): void { + $p->advance(); + + $userId = $user->getUID(); + $isEnabled = $this->config->getUserValue($userId, 'dav', 'generateBirthdayCalendar', 'yes'); + if ($isEnabled !== 'yes') { + return; + } + + /** @var IUser $user */ + $this->birthdayService->syncUser($user->getUID()); + }); + + $p->finish(); + $output->writeln(''); + return self::SUCCESS; + } + + protected function verifyEnabled(): void { + $isEnabled = $this->config->getAppValue('dav', 'generateBirthdayCalendar', 'yes'); + + if ($isEnabled !== 'yes') { + throw new \InvalidArgumentException('Birthday calendars are disabled'); + } + } +} diff --git a/apps/dav/lib/Command/SyncSystemAddressBook.php b/apps/dav/lib/Command/SyncSystemAddressBook.php new file mode 100644 index 00000000000..54edba01e05 --- /dev/null +++ b/apps/dav/lib/Command/SyncSystemAddressBook.php @@ -0,0 +1,44 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Command; + +use OCA\DAV\CardDAV\SyncService; +use OCP\IConfig; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\ProgressBar; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class SyncSystemAddressBook extends Command { + public function __construct( + private SyncService $syncService, + private IConfig $config, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('dav:sync-system-addressbook') + ->setDescription('Synchronizes users to the system addressbook'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $output->writeln('Syncing users ...'); + $progress = new ProgressBar($output); + $progress->start(); + $this->syncService->syncInstance(function () use ($progress): void { + $progress->advance(); + }); + + $progress->finish(); + $output->writeln(''); + $this->config->setAppValue('dav', 'needs_system_address_book_sync', 'no'); + return self::SUCCESS; + } +} |