aboutsummaryrefslogtreecommitdiffstats
path: root/apps/dav/lib/Command
diff options
context:
space:
mode:
Diffstat (limited to 'apps/dav/lib/Command')
-rw-r--r--apps/dav/lib/Command/ClearCalendarUnshares.php114
-rw-r--r--apps/dav/lib/Command/ClearContactsPhotoCache.php75
-rw-r--r--apps/dav/lib/Command/CreateAddressBook.php62
-rw-r--r--apps/dav/lib/Command/CreateCalendar.php93
-rw-r--r--apps/dav/lib/Command/CreateSubscription.php78
-rw-r--r--apps/dav/lib/Command/DeleteCalendar.php103
-rw-r--r--apps/dav/lib/Command/DeleteSubscription.php79
-rw-r--r--apps/dav/lib/Command/ExportCalendar.php95
-rw-r--r--apps/dav/lib/Command/FixCalendarSyncCommand.php73
-rw-r--r--apps/dav/lib/Command/GetAbsenceCommand.php62
-rw-r--r--apps/dav/lib/Command/ListAddressbooks.php76
-rw-r--r--apps/dav/lib/Command/ListCalendarShares.php131
-rw-r--r--apps/dav/lib/Command/ListCalendars.php76
-rw-r--r--apps/dav/lib/Command/ListSubscriptions.php77
-rw-r--r--apps/dav/lib/Command/MoveCalendar.php177
-rw-r--r--apps/dav/lib/Command/RemoveInvalidShares.php64
-rw-r--r--apps/dav/lib/Command/RetentionCleanupCommand.php28
-rw-r--r--apps/dav/lib/Command/SendEventReminders.php53
-rw-r--r--apps/dav/lib/Command/SetAbsenceCommand.php95
-rw-r--r--apps/dav/lib/Command/SyncBirthdayCalendar.php65
-rw-r--r--apps/dav/lib/Command/SyncSystemAddressBook.php48
21 files changed, 1549 insertions, 175 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 96ad350121c..9626edeba26 100644
--- a/apps/dav/lib/Command/CreateAddressBook.php
+++ b/apps/dav/lib/Command/CreateAddressBook.php
@@ -1,24 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Björn Schießle <bjoern@schiessle.org>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * 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;
@@ -30,38 +15,26 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class CreateAddressBook extends Command {
-
- /** @var IUserManager */
- private $userManager;
-
- /** @var CardDavBackend */
- private $cardDavBackend;
-
- /**
- * @param IUserManager $userManager
- * @param CardDavBackend $cardDavBackend
- */
- function __construct(IUserManager $userManager,
- CardDavBackend $cardDavBackend
+ public function __construct(
+ private IUserManager $userManager,
+ private CardDavBackend $cardDavBackend,
) {
parent::__construct();
- $this->userManager = $userManager;
- $this->cardDavBackend = $cardDavBackend;
}
- protected function configure() {
+ 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) {
+ protected function execute(InputInterface $input, OutputInterface $output): int {
$user = $input->getArgument('user');
if (!$this->userManager->userExists($user)) {
throw new \InvalidArgumentException("User <$user> in unknown.");
@@ -69,5 +42,6 @@ class CreateAddressBook extends Command {
$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
index 1cbd7b60944..033b5f8d347 100644
--- a/apps/dav/lib/Command/CreateCalendar.php
+++ b/apps/dav/lib/Command/CreateCalendar.php
@@ -1,62 +1,43 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author Thomas Citharel <tcit@tcit.fr>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * 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 {
-
- /** @var IUserManager */
- protected $userManager;
-
- /** @var IGroupManager $groupManager */
- private $groupManager;
-
- /** @var \OCP\IDBConnection */
- protected $dbConnection;
-
- /**
- * @param IUserManager $userManager
- * @param IGroupManager $groupManager
- * @param IDBConnection $dbConnection
- */
- function __construct(IUserManager $userManager, IGroupManager $groupManager, IDBConnection $dbConnection) {
+ public function __construct(
+ protected IUserManager $userManager,
+ private IGroupManager $groupManager,
+ protected IDBConnection $dbConnection,
+ ) {
parent::__construct();
- $this->userManager = $userManager;
- $this->groupManager = $groupManager;
- $this->dbConnection = $dbConnection;
}
- protected function configure() {
+ protected function configure(): void {
$this
->setName('dav:create-calendar')
->setDescription('Create a dav calendar')
@@ -68,7 +49,7 @@ class CreateCalendar extends Command {
'Name of the calendar');
}
- protected function execute(InputInterface $input, OutputInterface $output) {
+ protected function execute(InputInterface $input, OutputInterface $output): int {
$user = $input->getArgument('user');
if (!$this->userManager->userExists($user)) {
throw new \InvalidArgumentException("User <$user> in unknown.");
@@ -76,15 +57,31 @@ class CreateCalendar extends Command {
$principalBackend = new Principal(
$this->userManager,
$this->groupManager,
- \OC::$server->getShareManager(),
- \OC::$server->getUserSession()
+ 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->getLogger();
- $dispatcher = \OC::$server->getEventDispatcher();
-
+ $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, $this->groupManager, $random, $logger, $dispatcher);
+ $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
index 88f85a98812..db1ebb6ecb5 100644
--- a/apps/dav/lib/Command/SyncBirthdayCalendar.php
+++ b/apps/dav/lib/Command/SyncBirthdayCalendar.php
@@ -1,24 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * 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;
@@ -33,30 +18,15 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class SyncBirthdayCalendar extends Command {
-
- /** @var BirthdayService */
- private $birthdayService;
-
- /** @var IConfig */
- private $config;
-
- /** @var IUserManager */
- private $userManager;
-
- /**
- * @param IUserManager $userManager
- * @param IConfig $config
- * @param BirthdayService $birthdayService
- */
- function __construct(IUserManager $userManager, IConfig $config,
- BirthdayService $birthdayService) {
+ public function __construct(
+ private IUserManager $userManager,
+ private IConfig $config,
+ private BirthdayService $birthdayService,
+ ) {
parent::__construct();
- $this->birthdayService = $birthdayService;
- $this->config = $config;
- $this->userManager = $userManager;
}
- protected function configure() {
+ protected function configure(): void {
$this
->setName('dav:sync-birthday-calendar')
->setDescription('Synchronizes the birthday calendar')
@@ -65,11 +35,7 @@ class SyncBirthdayCalendar extends Command {
'User for whom the birthday calendar will be synchronized');
}
- /**
- * @param InputInterface $input
- * @param OutputInterface $output
- */
- protected function execute(InputInterface $input, OutputInterface $output) {
+ protected function execute(InputInterface $input, OutputInterface $output): int {
$this->verifyEnabled();
$user = $input->getArgument('user');
@@ -87,12 +53,12 @@ class SyncBirthdayCalendar extends Command {
$output->writeln("Start birthday calendar sync for $user");
$this->birthdayService->syncUser($user);
- return;
+ 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->callForAllUsers(function($user) use ($p) {
+ $this->userManager->callForSeenUsers(function ($user) use ($p): void {
$p->advance();
$userId = $user->getUID();
@@ -107,9 +73,10 @@ class SyncBirthdayCalendar extends Command {
$p->finish();
$output->writeln('');
+ return self::SUCCESS;
}
- protected function verifyEnabled () {
+ protected function verifyEnabled(): void {
$isEnabled = $this->config->getAppValue('dav', 'generateBirthdayCalendar', 'yes');
if ($isEnabled !== 'yes') {
diff --git a/apps/dav/lib/Command/SyncSystemAddressBook.php b/apps/dav/lib/Command/SyncSystemAddressBook.php
index e91ab38593d..54edba01e05 100644
--- a/apps/dav/lib/Command/SyncSystemAddressBook.php
+++ b/apps/dav/lib/Command/SyncSystemAddressBook.php
@@ -1,64 +1,44 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * 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 {
-
- /** @var SyncService */
- private $syncService;
-
- /**
- * @param SyncService $syncService
- */
- function __construct(SyncService $syncService) {
+ public function __construct(
+ private SyncService $syncService,
+ private IConfig $config,
+ ) {
parent::__construct();
- $this->syncService = $syncService;
}
- protected function configure() {
+ protected function configure(): void {
$this
->setName('dav:sync-system-addressbook')
->setDescription('Synchronizes users to the system addressbook');
}
- /**
- * @param InputInterface $input
- * @param OutputInterface $output
- */
- protected function execute(InputInterface $input, OutputInterface $output) {
+ protected function execute(InputInterface $input, OutputInterface $output): int {
$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();
});
$progress->finish();
$output->writeln('');
+ $this->config->setAppValue('dav', 'needs_system_address_book_sync', 'no');
+ return self::SUCCESS;
}
}